Projects‎ > ‎XBL‎ > ‎

XBL Scoping Improvements

STATUS: This is implemented in Orbeon Forms 4.0, except that event retargeting has been removed except for focus events.

Rationale

The Orbeon Forms XBL implementation is (as of August 2009 ) very strict on the scoping of identifiers and XPath expression resolution (variables).

In some cases, this is what is expected, e.g. a "currency" field must:
  • hide its internals to the outside world
  • not see too much of the outside world
This is justified by the fact that the control must be very reusable.

In other cases, such as:
  • accordion
  • tabs
  • datatable
the situation is a bit different, because the form author places a lot of markup within the bound node. E.g.:

<fr:accordion>
    <fr:case>
        <xhtml:div>
            Custom content, including XForms controls, etc.
        </xhtml:div>
    </fr:case>
    <fr:case>
        ...
    </fr:case>
</fr:accordion>

What happens in this case is that the content of fr:case is copied by the component into a separate DOM (called in XBL a shadow tree), which breaks the resolution logic as the form author would expect.

Scenarios for id resolution

Accessing an element by identifier from outside the component

<!-- This must reach the inner case -->
<xf:toggle case="my-case"/>
...
<fr:accordion>
    <fr:case>
        <fr:switch>
            <fr:case id="my-case">
              ...
            </fr:case>
        </fr:switch>
    </fr:case>
    <fr:case>
        ...
    </fr:case>
</fr:accordion>

Accessing an element by identifier from inside the component

<fr:switch>
    <fr:case id="my-case">
      ...
    </fr:case>
</fr:switch>
...
<fr:accordion>
    <fr:case>
        <fr:switch>
            <fr:case id="my-case">
                ...
                <!-- This must reach the outer case -->
                <xf:toggle case="my-case"/>
                ...
            </fr:case>
        </fr:switch>
    </fr:case>
    <fr:case>
        ...
    </fr:case>
</fr:accordion>

Same as above examples but with use of the index() function.

Variable scoping

<xxf:variable name="my-value" select="/foo/bar"/>
...
<fr:accordion>
    <fr:case>
        <fr:switch>
            <fr:case id="my-case">
                ...
                <!-- This must reach the outer variable -->
                <xf:output value="$my-value"/>
                ...
            </fr:case>
        </fr:switch>
    </fr:case>
    <fr:case>
        ...
    </fr:case>
</fr:accordion>


Scenarios for XPath resolution

Requirements:
  • Handling of LHHA elements within the bound element
  • Must be possible to copy it to within a group within the component
  • Resolution of single-node or node-set bindings must be possible, including @model, @bind, and reference to XPath variables

Simple control

Using the component, using id and XPath resolution:

<fr:foobar model="my-model" ref="$my-variable">
    <xforms:label ref="../my-label"/>
    <xforms:hint model="my-resources-model" ref="my-hint"/>
    <xforms:help ref="$my-help-message"/>
</fr:foobar>

Using xxbl:scope:

<xbl:template>
    <!-- Outer scope -->
    <xforms:group xxbl:scope="outer" xbl:attr="model context ref bind">
        <xbl:content includes="xforms|label, xforms|hint, xforms|alert, xforms|help"/>

        <!-- Inner scope -->
        <xforms:group xxbl:scope="inner" appearance="xxforms:internal">

            <!-- Variable is defined in inner scope, but can access the outer
                 scope's XPath context -->

            <xxforms:variable name="result" as="node()*">
                <xxforms:sequence select="." xxbl:scope="outer"/>
            </xxforms:variable>

            ... local implementation which now has access to $result ...

        </xforms:group>

    </xforms:group>
</xbl:template>


Strategy

  • System is aware of a notion of "scope" within a tree
    • scope applies to id resolution and XPath context
    • inner: scope is strictly within XBL shadow tree
    • outer: scope is that of bound element
  • Content copied with xbl:content is automatically marked in the shadow tree
    • E.g. xxbl:scope="outer | inner"
    • Unless xxbl:scope is overridden
  • Content copied with XSLT (xxbl:transform)
    • E.g. explicit adding of the xxbl:scope attribute
  • Subtree between xxbl:scope="outer" and xxbl:scope="inner" has following properties
    • Resolves ids and XPath against outer scope
    • Resolution "from the outside" can reach "inside"
      • E.g. xf:setindex, xf:setfocus, etc. running from outside the component
  • Outer scope
    • id visibility inherited from next outer
    • XPath visibility inherited from next outer
  • Component (inner) scope
    • id resolution is purely local
    • XPath resolution is purely local as well; if no local model, context is dummy root node (can be changed to empty context in the future)
    • access to outer context is done through variables

Events re-targeting

New principle:
  • If the target is in outer scope: the event is not re-targeted.
  • If the target is in inner scope: the event is re-targeted to the XBL component.

Use case: the autocomplete control.

  1. The autocomplete has a group bound to the node containing the value, which contains the LHHA. The LHHA are copied, and must be evaluated relative to that node. When the node becomes valid/invalid we want valid/invalid events to escape the autocomplete.
  2. The autocomplete contains an input field in inner scope. We want the focus in/out events to escape the autocomplete.
  3. The valid/invalid and focus in/out must be seen by the error summary as having the same target.

Implementing "by hand"

  1. Inside the autocomplete:

    <!-- Variable in inner scope pointing to bound node -->
    <xxforms:variable name="binding" as="node()?">
        <xxforms:sequence select="." xxbl:scope="outer" xbl:attr="model context ref bind"/>
    </xxforms:variable>

    <!-- Change the XPath context in outer scope, and block all events in outer scope -->
    <xforms:group ref="$binding" xxbl:scope="outer">
        <xforms:action ev:event="#all" ev:propagate="stop"/>

        <!-- Switch to inner scope: event on this group will propagate and be re-targeted -->
        <xforms:group ref="$binding" xxbl:scope="inner">
            <!-- LHHA evaluate in the right context, as we changed it earlier -->
            <xbl:content includes="fr|autocomplete > xforms|label, fr|autocomplete > xforms|help,
                                   fr|autocomplete > xforms|hint, fr|autocomplete > xforms|alert"/>
           
            <!-- Input in inner scope will see its focus in/out events re-targeted -->
            <xforms:input/>

        </xforms:group>
    </xforms:group>

  2. In the error summary, we switch to using event() instead of xxforms:event() to get the ids after re-targeting.

Automatic binding

The goal is for the XBL engine to do what the XBL author would otherwise need to do "by hand", as illustrated in the example above. The XBL engine understands and interprets the binding of an XBL component (context, model, bind, ref attributes):
  1. It changes the context item for the outer scope inside the component.
  2. A function, e.g. outer-context(), is provided to access the bound item from the inner scope. The default context in inner scope is still set to instance().
  3. If the bound item is a node, the XBL component automatically dispatches UI events for that node (e.g. xforms-invalid). The events can be listened to by users of the component, as well as <xbl:handlers> listeners inside the component.

Issues solved

  1. With the above structure (under Implementing "by hand") and the re-targeting of events from the inner scope, we get the following two events to be seen from outside of the component as coming from the component:
    • The xforms-valid and xforms-invalid coming from the bound group in inner scope.
    • The DOMFocusIn coming from an input in inner scope.
  2. With the automatic binding, we don't force XBL authors to copy the insanely verbose boilerplate we have under Implementing "by hand".

Re-target inner scope events

We have two types of XBL components:
  1. Components that are seen by users as containers, like the Form Builder section templates. Users of those controls see the controls they contain not as an implementation detail, but has distinct controls.
  2. Components that are seen as controls: if they use other controls, it is an implementation detail, and those controls shouldn't be visible users of the component.
This has an impact on whether events coming from controls in inner scope are re-targeted:
  1. They are not re-targeted for container components.
  2. They are re-targeted for control components.
This can be implemented as follows:
  1. XBL author can specify if they want re-targeting to be done, say with an attribute xxbl:retarget-inner-scope-events="true | false" with the default being true, as most of the "hand-made" components of the controls type.
  2. The section templates would set xxbl:retarget-inner-scope-events="false", while most of the other XBL components would keep the default, which automatically re-targets events from the inner scope.

Comments