Samples / Learn / Grid Table

Grid Table

Groups a sap.ui.table by the columns the user ticks - a subtotal row after each group, a grand total at the end, amounts summed per currency - and styles the total rows from their data.

Class
z2ui5_cl_smp_app_544
Repository
abap2UI5/samples
UI5 libraries
sap.m, sap.ui.core, sap.ui.table
Lowest UI5 release
1.71
In the browser
Runs in the playground
Controls it builds
sap.m.CheckBox, sap.m.HBox, sap.m.Label, sap.m.MessageStrip, sap.m.OverflowToolbar, sap.m.Page, sap.m.Shell, sap.m.Text, sap.m.Title, sap.m.ToolbarSpacer, sap.ui.core.CustomData, sap.ui.core.HTML, sap.ui.core.mvc.View, sap.ui.table.Column, sap.ui.table.RowSettings, sap.ui.table.Table

The class

" @keywords grid table subtotal grand total group sum currency rowsettings writetodom
" @summary Groups a sap.ui.table by the columns the user ticks - a subtotal row after each group, a grand total at the end, amounts summed per currency - and styles the total rows from their data.
CLASS z2ui5_cl_smp_app_544 DEFINITION PUBLIC.

  PUBLIC SECTION.
    INTERFACES z2ui5_if_app.

    TYPES:
      BEGIN OF ty_s_sale,
        region   TYPE string,
        country  TYPE string,
        category TYPE string,
        product  TYPE string,
        quantity TYPE i,
        amount   TYPE p LENGTH 15 DECIMALS 2,
        currency TYPE string,
        "! what the row is: a sale (empty), a subtotal or a grand total, see cs_kind
        kind     TYPE string,
      END OF ty_s_sale.
    TYPES ty_t_sales TYPE STANDARD TABLE OF ty_s_sale WITH EMPTY KEY.
    TYPES:
      BEGIN OF ty_s_column,
        name     TYPE string,
        label    TYPE string,
        selected TYPE abap_bool,
      END OF ty_s_column.

    CONSTANTS:
      BEGIN OF cs_kind,
        subtotal TYPE string VALUE `S`,
        total    TYPE string VALUE `T`,
      END OF cs_kind.

    DATA t_rows       TYPE ty_t_sales.
    DATA t_columns    TYPE STANDARD TABLE OF ty_s_column WITH EMPTY KEY.
    DATA visible_rows TYPE i.

    "! Sorts the rows by the grouping columns and inserts a subtotal row after
    "! each group and the grand total after the last row. Amounts only add up
    "! within one currency, so every unit column belongs to each group: a group
    "! in two currencies gets two subtotal rows, and so does the grand total.
    "! A subtotal row carries the values of its grouping columns, its units and
    "! the sums; everything else stays empty.
    "! @parameter group_by     | the grouping columns, outermost first; a name the rows do not have is ignored
    "! @parameter value_fields | the columns that are summed - amounts, quantities
    "! @parameter unit_fields  | the currency and unit columns
    "! @parameter kind_field   | the column that tells a subtotal or a total by cs_kind; optional
    "! @parameter rows         | a table of flat structures, sorted and extended in place
    METHODS subtotals_add
      IMPORTING
        group_by     TYPE string_table
        value_fields TYPE string_table
        unit_fields  TYPE string_table
        kind_field   TYPE string OPTIONAL
      CHANGING
        rows         TYPE STANDARD TABLE.

  PROTECTED SECTION.
    TYPES:
      BEGIN OF ty_s_sum,
        "! the number of grouping columns the sum is for; 0 is the grand total
        level TYPE i,
        row   TYPE REF TO data,
      END OF ty_s_sum.
    TYPES ty_t_sums TYPE STANDARD TABLE OF ty_s_sum WITH EMPTY KEY.

    DATA client         TYPE REF TO z2ui5_if_client.
    DATA t_sales        TYPE ty_t_sales.
    DATA t_group_fields TYPE string_table.
    DATA t_value_fields TYPE string_table.
    DATA t_unit_fields  TYPE string_table.
    DATA kind_column    TYPE string.

    METHODS on_init.
    METHODS view_display.
    METHODS rows_group.

    "! Adds the row to the sum of its units on the given level
    METHODS sum_add
      IMPORTING
        row   TYPE any
        level TYPE i
      CHANGING
        sums  TYPE ty_t_sums.

    "! The number of leading grouping columns in which both rows agree
    METHODS groups_common
      IMPORTING
        left          TYPE any
        right         TYPE any
      RETURNING
        VALUE(result) TYPE i.

    METHODS fields_equal
      IMPORTING
        left          TYPE any
        right         TYPE any
        fields        TYPE string_table
      RETURNING
        VALUE(result) TYPE abap_bool.

  PRIVATE SECTION.
ENDCLASS.


CLASS z2ui5_cl_smp_app_544 IMPLEMENTATION.

  METHOD z2ui5_if_app~main.

    me->client = client.

    IF client->check_on_init( ).
      on_init( ).
    ELSEIF client->check_on_navigated( ).
      view_display( ).
    ELSEIF client->check_on_event( `GROUP` ).
      rows_group( ).
    ENDIF.

  ENDMETHOD.


  METHOD on_init.

    t_sales = VALUE #(
        ( region = `EMEA`     country = `Germany`     category = `Hardware` product = `Notebook`         quantity = 10 amount = `12000.00` currency = `EUR` )
        ( region = `APAC`     country = `Japan`       category = `Software` product = `Antivirus`        quantity = 50 amount = `2500.00`  currency = `USD` )
        ( region = `EMEA`     country = `Switzerland` category = `Hardware` product = `Monitor`          quantity = 8  amount = `2400.00`  currency = `CHF` )
        ( region = `Americas` country = `USA`         category = `Hardware` product = `Server`           quantity = 2  amount = `18000.00` currency = `USD` )
        ( region = `EMEA`     country = `Germany`     category = `Software` product = `Office Suite`     quantity = 25 amount = `4500.00`  currency = `EUR` )
        ( region = `Americas` country = `Canada`      category = `Hardware` product = `Monitor`          quantity = 5  amount = `1500.00`  currency = `CAD` )
        ( region = `EMEA`     country = `France`      category = `Hardware` product = `Notebook`         quantity = 6  amount = `7200.00`  currency = `EUR` )
        ( region = `APAC`     country = `Australia`   category = `Services` product = `Training`         quantity = 2  amount = `3000.00`  currency = `USD` )
        ( region = `EMEA`     country = `Switzerland` category = `Services` product = `Installation`     quantity = 3  amount = `1350.00`  currency = `CHF` )
        ( region = `Americas` country = `USA`         category = `Services` product = `Support Contract` quantity = 12 amount = `6000.00`  currency = `USD` )
        ( region = `APAC`     country = `Japan`       category = `Hardware` product = `Notebook`         quantity = 4  amount = `4800.00`  currency = `USD` )
        ( region = `Americas` country = `Canada`      category = `Software` product = `Office Suite`     quantity = 40 amount = `7600.00`  currency = `CAD` ) ).

    " the order of this list is the order of the groups, the first ticked
    " column outermost
    t_columns = VALUE #(
        ( name = `REGION`   label = `Region`   selected = abap_true )
        ( name = `COUNTRY`  label = `Country`  selected = abap_false )
        ( name = `CATEGORY` label = `Category` selected = abap_false ) ).

    rows_group( ).
    view_display( ).

  ENDMETHOD.


  METHOD rows_group.

    DATA t_group_by TYPE string_table.

    LOOP AT t_columns INTO DATA(s_column) WHERE selected = abap_true.
      APPEND s_column-name TO t_group_by.
    ENDLOOP.

    t_rows = t_sales.
    subtotals_add( EXPORTING group_by     = t_group_by
                             value_fields = VALUE #( ( `AMOUNT` ) ( `QUANTITY` ) )
                             unit_fields  = VALUE #( ( `CURRENCY` ) )
                             kind_field   = `KIND`
                   CHANGING  rows         = t_rows ).

    " every row on screen: a scrolled-away grand total is easy to miss
    visible_rows = lines( t_rows ).

  ENDMETHOD.


  METHOD subtotals_add.

    DATA t_order   TYPE abap_sortorder_tab.
    DATA t_sums    TYPE ty_t_sums.
    DATA grouped   TYPE REF TO data.
    DATA field     TYPE string.
    DATA levels    TYPE i.
    DATA level     TYPE i.
    DATA kept      TYPE i.
    DATA s_sum     TYPE ty_s_sum.
    FIELD-SYMBOLS <grouped> TYPE STANDARD TABLE.
    FIELD-SYMBOLS <row>     TYPE any.
    FIELD-SYMBOLS <next>    TYPE any.
    FIELD-SYMBOLS <sum>     TYPE any.
    FIELD-SYMBOLS <field>   TYPE any.

    t_group_fields = VALUE #( ).
    t_value_fields = VALUE #( ).
    t_unit_fields  = VALUE #( ).
    kind_column    = to_upper( kind_field ).

    READ TABLE rows INDEX 1 ASSIGNING <row>.
    IF sy-subrc <> 0.
      RETURN.
    ENDIF.

    " a column the rows do not have groups nothing; a grouping column keeps
    " its value and is not summed
    LOOP AT group_by INTO field.
      field = to_upper( field ).
      ASSIGN COMPONENT field OF STRUCTURE <row> TO <field>.
      IF sy-subrc = 0 AND NOT line_exists( t_group_fields[ table_line = field ] ).
        APPEND field TO t_group_fields.
      ENDIF.
    ENDLOOP.
    IF t_group_fields IS INITIAL.
      RETURN.
    ENDIF.

    LOOP AT value_fields INTO field.
      field = to_upper( field ).
      ASSIGN COMPONENT field OF STRUCTURE <row> TO <field>.
      IF sy-subrc = 0 AND NOT line_exists( t_group_fields[ table_line = field ] ).
        APPEND field TO t_value_fields.
      ENDIF.
    ENDLOOP.
    LOOP AT unit_fields INTO field.
      field = to_upper( field ).
      ASSIGN COMPONENT field OF STRUCTURE <row> TO <field>.
      IF sy-subrc = 0.
        APPEND field TO t_unit_fields.
      ENDIF.
    ENDLOOP.

    LOOP AT t_group_fields INTO field.
      APPEND VALUE #( name = field ) TO t_order.
    ENDLOOP.
    LOOP AT t_unit_fields INTO field.
      APPEND VALUE #( name = field ) TO t_order.
    ENDLOOP.
    SORT rows STABLE BY (t_order).

    CREATE DATA grouped LIKE rows.
    ASSIGN grouped->* TO <grouped>.
    IF <grouped> IS NOT ASSIGNED.
      RETURN.
    ENDIF.
    levels = lines( t_group_fields ).

    LOOP AT rows ASSIGNING <row>.
      DATA(index) = sy-tabix.
      APPEND <row> TO <grouped>.
      DO levels + 1 TIMES.
        sum_add( EXPORTING row   = <row>
                           level = sy-index - 1
                 CHANGING  sums  = t_sums ).
      ENDDO.

      " the groups this row closes: every level deeper than the grouping
      " columns the next row still shares - after the last row all of them,
      " down to the grand total
      READ TABLE rows INDEX index + 1 ASSIGNING <next>.
      IF sy-subrc = 0.
        kept = groups_common( left = <row> right = <next> ).
      ELSE.
        kept = -1.
      ENDIF.

      level = levels.
      WHILE level > kept.
        LOOP AT t_sums INTO s_sum WHERE level = level.
          UNASSIGN <sum>.
          ASSIGN s_sum-row->* TO <sum>.
          IF <sum> IS ASSIGNED.
            APPEND <sum> TO <grouped>.
          ENDIF.
        ENDLOOP.
        DELETE t_sums WHERE level = level.
        level = level - 1.
      ENDWHILE.
    ENDLOOP.

    rows = <grouped>.

  ENDMETHOD.


  METHOD sum_add.

    DATA new_sum TYPE REF TO data.
    DATA s_sum   TYPE ty_s_sum.
    DATA t_keys  TYPE string_table.
    DATA field   TYPE string.
    FIELD-SYMBOLS <sum>    TYPE any.
    FIELD-SYMBOLS <source> TYPE any.
    FIELD-SYMBOLS <target> TYPE any.

    LOOP AT sums INTO s_sum WHERE level = level.
      UNASSIGN <sum>.
      ASSIGN s_sum-row->* TO <sum>.
      IF <sum> IS ASSIGNED AND fields_equal( left = <sum> right = row fields = t_unit_fields ) = abap_true.
        EXIT.
      ENDIF.
      UNASSIGN <sum>.
    ENDLOOP.

    IF <sum> IS NOT ASSIGNED.

      CREATE DATA new_sum LIKE row.
      ASSIGN new_sum->* TO <sum>.
      IF <sum> IS NOT ASSIGNED.
        RETURN.
      ENDIF.

      " the units and the grouping columns of this level
      t_keys = t_unit_fields.
      LOOP AT t_group_fields INTO field TO level.
        APPEND field TO t_keys.
      ENDLOOP.
      LOOP AT t_keys INTO field.
        ASSIGN COMPONENT field OF STRUCTURE row TO <source>.
        IF sy-subrc = 0.
          ASSIGN COMPONENT field OF STRUCTURE <sum> TO <target>.
          IF sy-subrc = 0.
            <target> = <source>.
          ENDIF.
        ENDIF.
      ENDLOOP.

      IF kind_column IS NOT INITIAL.
        ASSIGN COMPONENT kind_column OF STRUCTURE <sum> TO <target>.
        IF sy-subrc = 0.
          <target> = COND string( WHEN level = 0 THEN cs_kind-total ELSE cs_kind-subtotal ).
        ENDIF.
      ENDIF.
      INSERT VALUE #( level = level row = new_sum ) INTO TABLE sums.

    ENDIF.

    LOOP AT t_value_fields INTO field.
      ASSIGN COMPONENT field OF STRUCTURE row TO <source>.
      IF sy-subrc = 0.
        ASSIGN COMPONENT field OF STRUCTURE <sum> TO <target>.
        IF sy-subrc = 0.
          <target> = <target> + <source>.
        ENDIF.
      ENDIF.
    ENDLOOP.

  ENDMETHOD.


  METHOD groups_common.

    DATA field TYPE string.

    LOOP AT t_group_fields INTO field.
      IF fields_equal( left = left right = right fields = VALUE #( ( field ) ) ) = abap_false.
        RETURN.
      ENDIF.
      result = result + 1.
    ENDLOOP.

  ENDMETHOD.


  METHOD fields_equal.

    DATA field TYPE string.
    FIELD-SYMBOLS <left>  TYPE any.
    FIELD-SYMBOLS <right> TYPE any.

    LOOP AT fields INTO field.
      ASSIGN COMPONENT field OF STRUCTURE left TO <left>.
      IF sy-subrc <> 0.
        RETURN.
      ENDIF.
      ASSIGN COMPONENT field OF STRUCTURE right TO <right>.
      IF sy-subrc <> 0.
        RETURN.
      ENDIF.
      IF <left> <> <right>.
        RETURN.
      ENDIF.
    ENDLOOP.
    result = abap_true.

  ENDMETHOD.


  METHOD view_display.

    DATA t_layout TYPE STANDARD TABLE OF ty_s_column WITH EMPTY KEY.

    DATA(view) = z2ui5_cl_ui5_view_builder=>factory(
        )->ele( n = `View` ns = `mvc`
            )->a( n = `displayBlock` v = `true`
            )->a( n = `height`       v = `100%`
            )->a( n = `xmlns`        v = `sap.m`
            )->a( n = `xmlns:mvc`    v = `sap.ui.core.mvc`
            )->a( n = `xmlns:core`   v = `sap.ui.core`
            )->a( n = `xmlns:table`  v = `sap.ui.table` ).
    DATA(page) = view->ele( `Shell`
        )->ele( `Page`
            )->a( n = `title`          v = `abap2UI5 - Grid Table - Subtotals and Grand Total per Currency`
            )->a( n = `showNavButton`  b = client->check_app_prev_stack( )
            )->a( n = `navButtonPress` v = client->_event_nav_app_leave( ) ).

    page->tag( `MessageStrip`
        )->a( n = `text`     v = `Tick the columns to group by: the backend sorts the rows by them, inserts a subtotal row after ` &&
                   `each group and a grand total at the end. Amounts are summed per currency, so a group in two currencies ` &&
                   `gets two subtotal rows. Each cell writes its row kind into the DOM (CustomData writeToDom), and the ` &&
                   `style below colours the whole row from it.`
        )->a( n = `type`     v = `Information`
        )->a( n = `showIcon` b = abap_true
        )->a( n = `class`    v = `sapUiSmallMargin` ).

    " a grid table row takes no CSS class from its data, so every cell carries
    " the row kind as data-rowkind, and :has( ) lifts it to the row. The
    " theme parameters are CSS variables only on newer releases - the colour
    " after the comma is the fallback for the ones without
    page->tag( n = `HTML` ns = `core`
        )->a( n = `content` v = `<style>` && |\n| &&
                         `.sapUiTable tr:has([data-rowkind="S"]) > td \{ background-color: var(--sapInformationBackground, #f5faff); \}` && |\n| &&
                         `.sapUiTable tr:has([data-rowkind="T"]) > td \{ background-color: var(--sapWarningBackground, #fef7f1); \}` && |\n| &&
                         `[data-rowkind="S"], [data-rowkind="T"] \{ font-weight: bold; \}` && |\n| &&
                         `</style>` ).

    DATA(table) = page->ele( n = `Table` ns = `table`
        )->a( n = `rows`            v = client->_bind( t_rows )
        )->a( n = `selectionMode`   v = `None`
        )->a( n = `visibleRowCount` v = client->_bind( visible_rows )
        )->a( n = `class`           v = `sapUiSmallMarginBeginEnd` ).

    table->ele( n = `extension` ns = `table`
        )->ele( `OverflowToolbar`
            )->tag( `Title`
                )->a( n = `text` v = `Sales`
            )->tag( `ToolbarSpacer`
            )->tag( `Label`
                )->a( n = `text` v = `Group by:`
            )->ele( `HBox`
                )->a( n = `items` v = client->_bind( t_columns )
                )->tag( `CheckBox`
                    )->a( n = `text`     v = `{LABEL}`
                    )->a( n = `selected` v = `{SELECTED}`
                    )->a( n = `select`   v = client->_event( `GROUP` ) ).

    " the bar at the start of the row, in the colour of the row
    table->ele( n = `rowSettingsTemplate` ns = `table`
        )->tag( n = `RowSettings` ns = `table`
            )->a( n = `highlight` v = |\{= $\{KIND\} === '{ cs_kind-total }' ? 'Warning' : $\{KIND\} === '{ cs_kind-subtotal }' ? 'Information' : 'None' \}| ).

    " no sortProperty and no filterProperty: sorting in the browser would tear
    " the subtotals from their groups
    t_layout = VALUE #(
        ( name = `REGION`   label = `Region`   selected = abap_false )
        ( name = `COUNTRY`  label = `Country`  selected = abap_false )
        ( name = `CATEGORY` label = `Category` selected = abap_false )
        ( name = `PRODUCT`  label = `Product`  selected = abap_false )
        ( name = `QUANTITY` label = `Quantity` selected = abap_false )
        ( name = `AMOUNT`   label = `Amount`   selected = abap_false )
        ( name = `CURRENCY` label = `Currency` selected = abap_false ) ).

    DATA(columns) = table->ele( n = `columns` ns = `table` ).
    LOOP AT t_layout INTO DATA(s_layout).

      DATA(text) = |\{{ s_layout-name }\}|.
      DATA(align) = `Begin`.
      CASE s_layout-name.
        WHEN `PRODUCT`.
          text = |\{= $\{KIND\} === '{ cs_kind-total }' ? 'Total' : $\{KIND\} === '{ cs_kind-subtotal }' ? 'Subtotal' : $\{PRODUCT\} \}|.
        WHEN `QUANTITY`.
          align = `End`.
        WHEN `AMOUNT`.
          text  = `{ parts: ['AMOUNT', 'CURRENCY'], type: 'sap.ui.model.type.Currency', formatOptions: { showMeasure: false } }`.
          align = `End`.
      ENDCASE.

      DATA(column) = columns->ele( n = `Column` ns = `table`
          )->a( n = `width` v = `10rem` ).
      column->tag( `Label` )->a( n = `text` t = s_layout-label ).

      " writeToDom renders data-rowkind="S" on the cell's own element. It only
      " writes a string, so the expression turns an empty kind into I, an item
      column->ele( n = `template` ns = `table`
          )->ele( `Text`
              )->a( n = `text`      v = text
              )->a( n = `textAlign` v = align
              )->a( n = `wrapping`  b = abap_false
              )->ele( `customData`
                  )->tag( n = `CustomData` ns = `core`
                      )->a( n = `key`        v = `rowkind`
                      )->a( n = `value`      v = |\{= $\{KIND\} === '{ cs_kind-total }' ? 'T' : $\{KIND\} === '{ cs_kind-subtotal }' ? 'S' : 'I' \}|
                      )->a( n = `writeToDom` b = abap_true ).

    ENDLOOP.

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

  ENDMETHOD.

ENDCLASS.