Friday 8 April 2022

Exploring web app development with ABAP & htmx (in comparison with ABAP RAP)

1. Introduction

1.1 Motivation

The world is shifting towards SAP web development using SAPUI5 and ABAP RAP model on a platform like BTP, and it feels like all the years and efforts spent on developing pure ABAP applications and accumulating the know-how are starting to be forgotten.

The blog introduces a web app development using htmx to control the frontend and ABAP on the backend. To put this in the modern SAPUI5 MVC architecture, “view” is managed by htmx, “controller” is managed by ABAP, and “Model” is not needed because ABAP can directly access the database.

In this blog, I will go through my demo app to explain the interaction between htmx and ABAP, as well as how the CRUD operations can be handled. I aimed to create a business application which could be used by the real world business user, in order to share the feasibility and effort needed for this new development approach.

1.2 htmx in a Nutshell

The most concrete explanation and philosophy behind htmx can be found in the original blog. It’s got nice illustrations too. But in a Nutshell, what makes it special are:

◉ Any HTML element can issue an HTTP request to the backend

◉ Not only clicking can trigger HTTP request but also scrolling, as user enters value, and even with shortcut key

◉ Any element on the page can be updated(swapped) without updating the entire page

◉ A lot can be achieved on the frontend solely using htmx, without using JavaScript

If you are new to htmx(like me) and can’t wrap your head around these concepts, that’s ok! Nothing helps you understand better than getting your hands dirty!

2. Demo app

Let’s dive right into the demo app. This Sales Order Update App allows user to search the Sales order by certain selection filter and update line item information. The update result will be displayed on the Status column.

SAP ABAP Certification, SAP ABAP Career, SAP ABAP Skill, SAP ABAP Jobs, SAP ABAP Preparation, SAP ABAP Development, SAP ABAP

SAP ABAP Certification, SAP ABAP Career, SAP ABAP Skill, SAP ABAP Jobs, SAP ABAP Preparation, SAP ABAP Development, SAP ABAP

2.1 Implementation steps

Go to SE24 or SE80 and implement the backend ABAP class/methods. Here is the code of this app.

Create a new custom service in SICF and activate it. In this demo, I named my class “ZHTMX_HANDLER_SO” so that class should be set in your service. Right click the service and “Test Service” will open the app on your default browser.

2.2 Code explanation

2.2.1 Initial call

When the Service URL is accessed, the class will be called and method “IF_HTTP_EXTENSION~HANDLE_REQUEST” is triggered. Here, first we get the query string and form data send on the request from the browser. In the initial call there is no string query or form so will come back to that. Then Depending on the method type, the code goes to different logic. The initial call will be GET and “hx–request” will always be empty. hx–request will be true if the request is triggered by htmx element such as “hx–get” and hx-put“. So the code will call 4 different methods(HTML_PAGE, HTML_SHELLBAR, HTML_SELECTION, HTML_TABLE) in order to create the first view of the page. Each methods appends the html fragments into variable “HTML”, which is also their return value. SERVER->RESPONSE->APPEND_CDATA will return the whole HTML code back to browser as the Response.

METHOD IF_HTTP_EXTENSION~HANDLE_REQUEST.

    "Get Request parameters
    ME->GV_PATH = SERVER->REQUEST->GET_HEADER_FIELD(
                 IF_HTTP_HEADER_FIELDS_SAP=>PATH_TRANSLATED_EXPANDED ).

    "Get the query value Order type
    ME->GV_AUART = ESCAPE( VAL = SERVER->REQUEST->GET_FORM_FIELD( `a` )
                                  FORMAT = CL_ABAP_FORMAT=>E_HTML_TEXT ).
    "Get the query value Sales Org
    ME->GV_VKORG = ESCAPE( VAL = SERVER->REQUEST->GET_FORM_FIELD( `v` )
                                  FORMAT = CL_ABAP_FORMAT=>E_HTML_TEXT ).

    "Get the SO number user is operating
    ME->GV_SO_KEY = ESCAPE( VAL = SERVER->REQUEST->GET_FORM_FIELD( `k` )
                                  FORMAT = CL_ABAP_FORMAT=>E_HTML_TEXT ).

    "Get form data
    CALL METHOD SERVER->REQUEST->GET_FORM_FIELDS( CHANGING FIELDS = GT_FORM ).

    "Get method
    IF SERVER->REQUEST->GET_HEADER_FIELD(
         IF_HTTP_HEADER_FIELDS_SAP=>REQUEST_METHOD ) = `GET`.

      IF SERVER->REQUEST->GET_HEADER_FIELD( `hx-request` ) IS INITIAL.

        SERVER->RESPONSE->APPEND_CDATA( HTML_PAGE( ) ).
        SERVER->RESPONSE->APPEND_CDATA( HTML_SHELLBAR( ) ).
        SERVER->RESPONSE->APPEND_CDATA( HTML_SELECTION( ) ).
        SERVER->RESPONSE->APPEND_CDATA( HTML_TABLE( EXPORTING FORM = GT_FORM ) ).

      ELSE.

        CASE SERVER->REQUEST->GET_HEADER_FIELD( `app-action` ).

          WHEN `search_init`.
            "Implement Search Init logic of your choice

          WHEN `scroll`.

            GV_PAGE = SERVER->REQUEST->GET_HEADER_FIELD( `app-page` ).
            SERVER->RESPONSE->APPEND_CDATA( HTML_TABLE_ROWS( EXPORTING FORM = GT_FORM ) ).

          WHEN `edit`.
            SERVER->RESPONSE->APPEND_CDATA( HTML_EDIT_ROWS( EXPORTING FORM = GT_FORM ) ).

          WHEN `cancel`.
            SERVER->RESPONSE->APPEND_CDATA( HTML_CANCEL_EDIT( EXPORTING FORM = GT_FORM ) ).

        ENDCASE.

        DATA(LV_ACTION) = SERVER->REQUEST->GET_HEADER_FIELD( `app-action` ).

      ENDIF.

      SERVER->RESPONSE->SET_STATUS( CODE = 200
                                    REASON = IF_HTTP_STATUS=>REASON_200 ).
      SERVER->RESPONSE->SET_CONTENT_TYPE( `text/html` ).

    ENDIF.

    "Put Method
    IF SERVER->REQUEST->GET_HEADER_FIELD(
               IF_HTTP_HEADER_FIELDS_SAP=>REQUEST_METHOD ) = `PUT`.
      CASE SERVER->REQUEST->GET_HEADER_FIELD( `app-action` ).

        WHEN `search`.
          SERVER->RESPONSE->APPEND_CDATA( HTML_TABLE( EXPORTING FORM = GT_FORM ) ).

        WHEN `save`.
          SERVER->RESPONSE->APPEND_CDATA( HTML_SAVE_EDIT( EXPORTING FORM = GT_FORM
                                                          IMPORTING STATUS_MSG = GV_STATUS_MSG ) ).
      ENDCASE.

      SERVER->RESPONSE->SET_STATUS( CODE = 200
                                    REASON = IF_HTTP_STATUS=>REASON_200 ).
      SERVER->RESPONSE->SET_CONTENT_TYPE( `text/html` ).
    ENDIF.

  ENDMETHOD.

2.2.2 Fundamental library and htmx library

In HTML_PAGE, the header of the app page is defined. The important point here is that Fundamental Library and htmx library are defined. In the later section of the code, you will find html class such as “fd-table” and “fd-input” and attributes such as “hx–headers” and “hx–target”. “fd-” are referring to Fundamental library to give SAP Fiori theme UI and “hx-” attributes enables htmx function from htmx library.

`<link href='https://unpkg.com/fundamental-styles@0.22.0/dist/fundamental-styles.css' rel='stylesheet'>`
`<script src="https://unpkg.com/htmx.org@latest/dist/htmx.js"></script>`

2.2.3 Frontend/Backend interaction by htmx

By pressing the search button, PUT request is triggered with the service URL of this app. This behavior is defined in HTML_SELECTION. “data–hx–put=” defines the service to be triggered when the search button is pressed. In this case, GV_PATH contains the URL path initially triggered, and that will be triggered again when the button is pressed. Whatever set in “hx–headers=” will be added to Request header when the service is triggered. In this case, “app–action = search” will be added on the header and when the service is triggered by “data–hx–put”, the code will process into the case statement where “WHEN `search` = true” in the main HANDLE_REQUEST method. In other words, whatever set in “hx–headers” works like a OK code in ABAP dynpro and it gets picked up in the main HANDLE_REQUEST method.

Another point to mention here is that this button is inside a “form”, which means that the input values(Document type and Sales Org) will be passed as form data on the Request when the search button is pressed. Form data is fetched by method GET_FORM_FIELDS in the main HANDLE_REQUEST and later used to filter the SELECT statement. The purpose of those input fields are to filter the output of sales orders.

  METHOD HTML_SELECTION.

    CONCATENATE
     `<form class="fd-form__item">`
       `<label class="fd-form__label" for="auart"> Document type </label>`
       `<input id="auart" class="fd-input fd-input-group__input" style="max-width: 100px;" type="text" name="Document type" >`
       `<label class="fd-form__label" for="vkorg"> Sales Org </label>`
       `<input id="vkorg" class="fd-input fd-input-group__input" style="max-width: 100px;" type="text" name="Sales Org" >`
         `<button type="button" class="fd-button" `
                  `aria-label="clear search" `
                  `data-hx-get="` GV_PATH `" `
                  `data-hx-headers='{"app-action": "search_init"}'>`
            `<i class="sap-icon--clear-all"></i>`
          `</button>`
         `<button type="button" class="fd-button" `
                  `aria-label="saerch" `
                  `data-hx-target="#sap_tables" `
                  `data-hx-swap-oob="true" `
                  `data-hx-put="` GV_PATH `" `
                  `data-hx-headers='{"app-action": "search"}'>`
            `<i class="sap-icon--search"></i>`
          `</button>`
     `</form>`
    INTO HTML.


 ENDMETHOD.

Similarly, Edit button on each table rows are also calling back the class method every time they are pressed. Here, form is not used but “hx–vals” allows you to add search string to the request and in this case, search string k with value of GV_SO_KEY is passed. GV_SO_KEY contains the sales order + item number key which is concatenated before TAB_ROW_DISP_MODE is called. This is for the callback program to identify which sales order row the user is trying to change.

 METHOD TAB_ROW_DISP_MODE.

    "Put the row to display mode
    CONCATENATE HTML
    `<tr class="fd-table__row fd-table__row--hoverable">`
      `<td class="fd-table__cell">` DATA-VBELN `</td>`
      `<td class="fd-table__cell">` DATA-AUART `</td>`
      `<td class="fd-table__cell">` DATA-VKORG `</td>`
      `<td class="fd-table__cell">` DATA-POSNR `</td>`
      `<td class="fd-table__cell">` DATA-MATNR `</td>`
      `<td class="fd-table__cell">` DATA-KWMENG `</td>`
      `<td class="fd-table__cell">` DATA-VRKME `</td>`
      `<td>`
        `<button class="btn btn-danger"`
                `data-hx-vals='{"k": "` GV_SO_KEY `"}' `
                `data-hx-get="` GV_PATH `" `
                `data-hx-headers='{"app-action": "edit"}' >`
          `Edit`
        `</button>`
      `</td>`
      `<td class="fd-table__cell fd-info-label--accent-color-6" style="margin:0.2rem"">` STATUS_MSG `</td>`
    `</tr>` INTO HTML.

  ENDMETHOD.

Other htmx initiated user interactions include, pressing cancel buttons, pressing save button and scrolling down to fetch more records. They all call back the backend ABAP class method and return with the response data.

2.2.4 Swapping in htmx

One of the attractive feature of htmx is that you can swap and replace certain part of the page without returning the whole page of HTML. This is achieved by first defining the default swapping mode. “outerHTML” replaces the entire HTML element that is in process. Here you can find other swapping modes.

<meta name="htmx-config" content='{"defaultSwapStyle":"outerHTML"}'>

Let’s look at an example when user presses Edit on one of the rows. WHEN `edit` will be true and HTML_EDIT_ROWS is called, in which calls TAB_ROW_EDIT_MODE. This method is only returning one row of table in HTML, NOT the whole page of the app starting from the Shell bar to the bottom of the table.

OuterHTML swapping is used almost everywhere in this app, but the search button is using different kind of swapping. hx-swap-oob comes in handy when you want to swap other part of the page instead of itself. When search button is pressed, no change on the button itself is required but the table contents below should be refreshed. So that’s why hx–target=“#sap_tables” is set, which means this button is intended to swap the contents of “sap_tables”, which is the id for the HTML table. 

         `<button type="button" class="fd-button" `
                  `aria-label="saerch" `
                  `data-hx-target="#sap_tables" `
                  `data-hx-swap-oob="true" `
                  `data-hx-put="` GV_PATH `" `
                  `data-hx-headers='{"app-action": "search"}'>`
            `<i class="sap-icon--search"></i>`
          `</button>`

2.3 CRUD operation

This app uses Read and Update of the sales document data so I will only highlight how Create and Delete can by handled.

Create: Define a “Add” button with attribute “hx–headers=‘{“app-action”: “add”}'” and set app’s callback URL on “hx–get“. In the main HANDLE_REQUEST method, create a CASE statement for “add” and call BAPI or BDC of your choice to create sales order. 

Delete: Define a “Delete” button on each row of the table with attribute “hx–headers='{“app-action”: “delete”}'” and set app’s callback URL on “hx–get“. The rest is same as Create. Create your own logic to delete the sales order item. One thing you have to remember is that nothing should be returned by APPEND_CDATA on the request’s response to make sure that the deleted item will be removed from the HTML table.

So it’s very straightforward! Just need to use htmx initiated request & htmx swapping wisely and combine it with backend ABAP to update the SAP database.

2.4 Exercise(Try it!)

Now I assume you have pretty good idea how swapping and htmx initiated request work. If you’d like, you can implement your own logic for the “search clear button”, as I left it empty on purpose.

          WHEN `search_init`.
            "Implement Search Init logic of your choice
 

3. Compatibility with ABAP


Through building this app, here is my summary of this development approach.

◉ Full access to objects in SAP application/DB server through ABA
◉ No need to call API to fetch dataset thus no need to create/manage Odata in backend
◉ Entire code is wrapped in one ABAP class for good maintainability (but you can always setup multiple services and classes in SICF)
◉ The frontend code will be very light. All the complex logic is managed in backend ABAP

◉ Input fields cannot directly refer to SAP data domain thus no automatic external/internal value conversion(had to manually convert doc number, sales org, etc.) 
◉ A lot of effort to create select-options and F4 value help. They’re so easy in ABAP though…
◉ Need better idea to maintain HTML code than using string concatenation -> In the next release, it would be cool if SAP integrated HTML editor in SE80.

There are pros and cons but since it’s a new approach, there is always room for improvement!

4. Comparing the efforts with ABAP RAP Model


This new development model offers new possibilities for SAP web development and all, but does it save more time and hassle compared to the existing web app development framework? I used ABAP RAP to create nearly the identical app and turned out it took much less effort to build it.

On top of that, ABAP RAP model does not have the weakness of htmx & ABAP model, which are:

◉ Internal/external format conversion is performed automatically
◉ F4 search help comes as default
◉ Select options/multiple selection are possible

on top of these advantages,

◉ DB data is at your fingertip. BAPI call also possible to update database(not in the CRUD methods but only in the saver methods)
◉ Coding is only needed on behavior class, which was less than 200 lines

SAP ABAP Certification, SAP ABAP Career, SAP ABAP Skill, SAP ABAP Jobs, SAP ABAP Preparation, SAP ABAP Development, SAP ABAP

SAP ABAP Certification, SAP ABAP Career, SAP ABAP Skill, SAP ABAP Jobs, SAP ABAP Preparation, SAP ABAP Development, SAP ABAP

The only drawback I felt there was is that I couldn’t call BAPI anywhere I wanted in the behavior class method. BAPI parameters must be prepared in the CRUD methods and pass them as ABAP memory to the saver methods.

Source: sap.com

No comments:

Post a Comment