Skip to content

XML Templating

XML Templating is a UI5 preprocessor feature, not an abap2UI5 invention. The UI5 runtime understands a small set of instructions in the template XML namespace — template:repeat, template:if, template:then, template:else, template:with — and expands them into plain XML before the control tree is created. abap2UI5 exposes these instructions through the fluent builder so you can drive the expansion from ABAP data.

See the official UI5 references for the underlying mechanics: XML Templating, template:repeat, template:if.

How It Works

Templating happens once, at view instantiation, against a JSON model. The preprocessor walks the XML, evaluates each template: instruction against that model, and replaces the instruction with the resulting XML. After that the control tree is built from the expanded XML and normal data binding takes over.

This timing has two consequences:

  • Expansion is build-time. A template:repeat over an internal table produces a fixed number of controls; the expanded XML is what UI5 renders.
  • Data changes do not re-template. If the data driving the template changes, the existing expansion stays as is. To pick up the change you must rebuild the view (view_display) or the templated fragment (nest_view_display) — see Re-rendering below.

abap2UI5 wires up the templating model for you. Every variable you bind with client->_bind( ... ) is reachable inside templates via the template> model prefix:

ABAP bindingPath inside templates
client->_bind( mt_layout ){template>/MT_LAYOUT}
client->_bind( mv_flag ){template>/MV_FLAG}

The template> model is the templating engine's view of the data — distinct from the default model used by runtime bindings like {MT_DATA}.

template:repeat — Loops

template:repeat clones its children once per row of the bound list. Use it when the structure of the view (e.g. which columns a table has) depends on data:

abap
mt_layout = VALUE #( ( fname = `NAME` merge = `false` visible = `true`  )
                     ( fname = `DATE` merge = `false` visible = `true`  )
                     ( fname = `AGE`  merge = `false` visible = `false` ) ).
client->_bind( mt_layout ).

view->ele( `Table`
    )->a( n = `items` v = client->_bind( mt_data )

    )->ele( `columns`
        )->ele( n = `repeat` ns = `template`
            )->a( n = `list` v = `{template>/MT_LAYOUT}`
            )->a( n = `var`  v = `L0`

            )->ele( `Column`
                )->a( n = `mergeDuplicates` v = `{L0>MERGE}`
                )->a( n = `visible`         v = `{L0>VISIBLE}`

                )->tag( `Text`
                    )->a( n = `text` v = `{L0>FNAME}`
            )->end(
        )->end(
    )->end(

    )->ele( `items`
        )->ele( `ColumnListItem`
            )->ele( `cells`
                )->ele( n = `repeat` ns = `template`
                    )->a( n = `list` v = `{template>/MT_LAYOUT}`
                    )->a( n = `var`  v = `L1`

                    )->tag( `ObjectIdentifier`
                        )->a( n = `text` v = `{= '{' + ${L1>FNAME} + '}' }` ).

Notes on the snippet:

  • list is the binding path that drives the loop; var is the alias used inside the loop body (here L0 for the column headers, L1 for the cells). template is an element namespace like any other, so the builder needs nothing beyond ns = template on the element — the prefix itself is declared once on the view's root.
  • Inside the loop, {L0>FNAME} is a templating-time read — it ends up as the literal string NAME/DATE/AGE in the expanded XML.
  • {= '{' + ${L1>FNAME} + '}' } is an expression binding that constructs another binding string at templating time. With L1>FNAME = NAME it expands to text="{NAME}", which becomes a normal runtime binding against the row of mt_data. This is the standard pattern for templated tables: outer loop builds the columns, inner loop builds the cells, expression binding wires each cell to the right field of the row.
  • template_repeat accepts the same optional attributes as the UI5 instruction (startIndex, length) plus list-binding extras like sorters and filters.

The full sample is Z2UI5_CL_SMP_APP_173.

The Whole Thing, Runnable

The fragments above are two halves of one view. Press Run to see the expansion happen — three rows of data, three columns built by the outer loop, and each cell wired to its field by the inner one:

abap
CLASS z2ui5_cl_sample_templating DEFINITION PUBLIC.

  PUBLIC SECTION.
    INTERFACES z2ui5_if_app.

    TYPES:
      BEGIN OF ty_s_layout,
        fname   TYPE string,
        merge   TYPE string,
        visible TYPE string,
      END OF ty_s_layout.

    TYPES:
      BEGIN OF ty_s_row,
        name TYPE string,
        date TYPE string,
        age  TYPE string,
      END OF ty_s_row.

    DATA mt_layout TYPE STANDARD TABLE OF ty_s_layout WITH EMPTY KEY.
    DATA mt_data   TYPE STANDARD TABLE OF ty_s_row    WITH EMPTY KEY.

  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.

CLASS z2ui5_cl_sample_templating IMPLEMENTATION.
  METHOD z2ui5_if_app~main.

    IF client->check_on_navigated( ).

      mt_layout = VALUE #( ( fname = `NAME` merge = `true`  visible = `true` )
                           ( fname = `DATE` merge = `false` visible = `true` )
                           ( fname = `AGE`  merge = `false` visible = `true` ) ).

      mt_data = VALUE #( ( name = `Alice` date = `2026-01-15` age = `34` )
                         ( name = `Bob`   date = `2026-02-03` age = `28` )
                         ( name = `Cleo`  date = `2026-03-21` age = `41` ) ).

      DATA(view) = z2ui5_cl_ui5_view_builder=>factory(
          )->ele( n = `View` ns = `mvc`
              )->a( n = `xmlns`          v = `sap.m`
              )->a( n = `xmlns:mvc`      v = `sap.ui.core.mvc`
              )->a( n = `xmlns:template` v = `http://schemas.sap.com/sapui5/extension/sap.ui.core.template/1`

              )->ele( `Page`
                  )->a( n = `title` v = `XML Templating`

                  )->ele( `Table`
                      )->a( n = `items` v = client->_bind( mt_data )

                      )->ele( `columns`
                          )->ele( n = `repeat` ns = `template`
                              )->a( n = `list` v = `{template>/MT_LAYOUT}`
                              )->a( n = `var`  v = `L0`

                              )->ele( `Column`
                                  )->a( n = `mergeDuplicates` v = `{L0>MERGE}`
                                  )->a( n = `visible`         v = `{L0>VISIBLE}`

                                  )->tag( `Text`
                                      )->a( n = `text` v = `{L0>FNAME}`

                              )->end(

                          )->end(

                      )->end(

                      )->ele( `items`
                          )->ele( `ColumnListItem`
                              )->ele( `cells`
                                  )->ele( n = `repeat` ns = `template`
                                      )->a( n = `list` v = `{template>/MT_LAYOUT}`
                                      )->a( n = `var`  v = `L1`

                                      )->tag( `ObjectIdentifier`
                                          )->a( n = `text` v = `{= '{' + ${L1>FNAME} + '}' }` ) ).

      client->view_display( view->stringify( ) ).

    ENDIF.

  ENDMETHOD.
ENDCLASS.

Flip a row's visible to false and re-run: the column is gone from the expanded XML entirely, not merely hidden.

template:if / template:then / template:else — Conditionals

template:if evaluates an expression against the templating model and keeps or drops its children accordingly. With a template:then / template:else pair you get a two-branch switch:

abap
client->_bind( mv_flag ).

view->ele( n = `if` ns = `template`
    )->a( n = `test` v = `{template>/MV_FLAG}`

    )->ele( n = `then` ns = `template`
        )->tag( n = `Icon` ns = `core`
            )->a( n = `src`   v = `sap-icon://accept`
            )->a( n = `color` v = `green`
    )->end(

    )->ele( n = `else` ns = `template`
        )->tag( n = `Icon` ns = `core`
            )->a( n = `src`   v = `sap-icon://decline`
            )->a( n = `color` v = `red` ).

The test argument follows the same rules as in UI5: any binding expression is fine, and the string "false" is treated as boolean false (a UI5 convenience). For richer conditions use expression binding, e.g. `{= ${template>/MV_COUNT} > 0 }`.

template:elseif is also supported by UI5, and needs nothing extra here: it is the same ele call with a different name, ns = template and a test attribute. That is the point of the builder — every templating instruction UI5 has is reachable the moment UI5 has it, without waiting for a method.

Re-rendering

Because templating runs once, the view must be rebuilt for changes in the templating model to take effect. There are two strategies:

Rebuild the whole view — simplest, works for most apps. After the user toggles a flag, render the view again:

abap
CASE client->get( )-event.
  WHEN `CHANGE_FLAG`.
    view_display( ).   " builds and ships the view again
ENDCASE.

This is the pattern used in Z2UI5_CL_SMP_APP_173: the switch fires CHANGE_FLAG, view_display runs, the new value of mv_flag flows through template:if, and the icon swaps.

Rebuild only the templated fragment — keeps a stable shell on screen and replaces a nested piece. Z2UI5_CL_SMP_APP_176 separates the main view from the templated table:

abap
" Main view: built once, stays on screen
DATA(view) = z2ui5_cl_ui5_view_builder=>factory(
    )->ele( n = `View` ns = `mvc`
        )->a( n = `xmlns`     v = `sap.m`
        )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc`

        )->ele( `Shell`
            )->ele( `Page`
                )->a( n = `title` v = `Main View`
                )->a( n = `id`    v = `test` ).   " ...

client->view_display( view->stringify( ) ).

" Nested templated view: inserted into the main view by id.
" The template namespace has to be declared on the nested view's root.
DATA(nested) = z2ui5_cl_ui5_view_builder=>factory(
    )->ele( n = `View` ns = `mvc`
        )->a( n = `xmlns`          v = `sap.m`
        )->a( n = `xmlns:mvc`      v = `sap.ui.core.mvc`
        )->a( n = `xmlns:template` v = `http://schemas.sap.com/sapui5/extension/sap.ui.core.template/1`

        )->ele( `Shell`
            )->ele( `Page`
                )->a( n = `title` v = `Nested View`

                )->ele( `Table`
                    )->a( n = `items` v = client->_bind( mt_data )

                    )->ele( `columns`
                        )->ele( n = `repeat` ns = `template`
                            )->a( n = `list` v = `{template>/MT_LAYOUT}`
                            )->a( n = `var`  v = `L0`

                            )->ele( `Column` ).   " ...

client->nest_view_display( val           = nested->stringify( )
                           id            = `test`
                           method_insert = `addContent` ).

nest_view_display targets a control in the existing view by id (test) and appends/replaces the nested view there. To refresh the templated piece on a data change, call nest_view_display again from the event handler — the main view is left untouched.

Templating vs ABAP-side Composition

The two demo apps both build dynamic views, but with different mechanics. Pick based on what the dynamic part actually is:

SituationApproach
Whether a control exists at all, decided once per renderABAP IF around the builder call — simpler, no template: namespace involved
Number of controls comes from an internal table, computed onceABAP LOOP over the table, calling the builder for each row
Reusable XML fragment that UI5 itself should expand against metadatatemplate:repeat / template:if — keeps the templating logic in the view layer
Cells of a table where the column set itself is data-driventemplate:repeat — UI5 expands columns and cell bindings in one pass, as in app 173

Plain ABAP control flow covers most cases and is easier to debug. Reach for template: when you want the expansion to live in the view (closer to standard UI5 patterns) or when you are mapping a metadata-style structure onto controls.

Tips

  • Always bind the data that drives a template (client->_bind) before the builder call that references it. The binding registers the path that {template>/...} resolves against.
  • Inside template:repeat, prefer {var>FIELD} over deeper paths — it keeps the body readable and lets you nest repeats with distinct var names (L0, L1, ...) without collisions.
  • Use expression binding ({= ... }) when the value you need is a binding string itself. Templating-time expressions can read ${var>...} and concatenate strings, which is how dynamic cell bindings are assembled.
  • If a templated control does not update after a data change, you forgot to rebuild — call view_display or nest_view_display again.

See Z2UI5_CL_SMP_APP_173 for template:repeat + template:if in a single view and Z2UI5_CL_SMP_APP_176 for the stable-shell / templated-nested-view pattern.

Working Samples

Complete apps from the sample catalogue that use what this page describes. Each is a single class — pull the repository with abapGit and start it with ?app_start=<class>.

SampleClass
Build Columns Dynamically (template:repeat)Z2UI5_CL_SMP_APP_173
Dynamic Content in a Nested ViewZ2UI5_CL_SMP_APP_176