A step-by-step tutorial to learn abap2UI5 — build SAP UI5 apps purely in ABAP, no JavaScript, OData or RAP required.
The tutorial consists of seven lessons. Each lesson is a small, self-contained app that builds on the concepts of the previous one — from a simple input form to a table application with create, delete and confirmation popups backed by a database table.
| Lesson | Class | Branch | Topic |
|---|---|---|---|
| 01 | z2ui5_cl_tutorial_01 |
lesson-01 |
Selection screen with data input |
| 02 | z2ui5_cl_tutorial_02 |
lesson-02 |
Internal table displayed with sap.m.Table |
| 03 | z2ui5_cl_tutorial_03 |
lesson-03 |
Database table read into an internal table |
| 04 | z2ui5_cl_tutorial_04 |
lesson-04 |
Filtering and sorting the table |
| 05 | z2ui5_cl_tutorial_05 |
lesson-05 |
Toolbar, selecting and deleting an entry |
| 06 | z2ui5_cl_tutorial_06 |
lesson-06 |
Confirmation popup before deleting |
| 07 | z2ui5_cl_tutorial_07 |
lesson-07 |
Creating a new entry with a popup form |
z2ui5_cl_tutorial_00 is an overview app that lists all lessons and navigates to them. It ships with branch lesson-07 (and main), because it references all lesson classes.
Install the abap2UI5 framework with abapGit and set up the HTTP service as described in the documentation.
Every lesson has its own branch. Branch lesson-NN contains the repository up to and including lesson NN — so you can pull the lessons into your system one at a time, always working with exactly the objects you have already learned about:
| Branch | Contains | New objects |
|---|---|---|
lesson-01 |
Lesson 01 | z2ui5_cl_tutorial_01 |
lesson-02 |
Lessons 01–02 | z2ui5_cl_tutorial_02 |
lesson-03 |
Lessons 01–03 | z2ui5_cl_tutorial_03, table Z2UI5_T_TUTORIAL |
lesson-04 |
Lessons 01–04 | z2ui5_cl_tutorial_04 |
lesson-05 |
Lessons 01–05 | z2ui5_cl_tutorial_05 |
lesson-06 |
Lessons 01–06 | z2ui5_cl_tutorial_06 |
lesson-07 |
Lessons 01–07 | z2ui5_cl_tutorial_07, overview app z2ui5_cl_tutorial_00 |
- In abapGit choose New Online, enter this repository's URL, select branch
lesson-01and a separate package, then pull. - Activate the objects and work through lesson 01 (see below).
- When you are ready for the next lesson: in abapGit choose Switch branch, select
lesson-02and pull again — only the new objects of lesson 02 are added, everything you already pulled stays untouched. - Repeat for each lesson. From branch
lesson-03onwards, remember to activate the new database tableZ2UI5_T_TUTORIALas well.
Install the main branch with abapGit into a separate package and activate all objects — including the database table Z2UI5_T_TUTORIAL used from lesson 03 onwards.
Start an app by appending ?app_start=z2ui5_cl_tutorial_01 (or any other lesson class — z2ui5_cl_tutorial_00 for the overview) to the URL of your abap2UI5 HTTP service.
Every app is a plain ABAP class that implements z2ui5_if_app with a single method main. The framework calls main on every roundtrip (HTTP POST from the browser). Inside main you check the lifecycle situation and react to it:
IF client->check_on_init( ).
" first call: set up data, display the view
ELSEIF client->check_on_event( ).
" the user triggered an event (button press, search, ...)
ENDIF.Views are XML strings built with the fluent builder z2ui5_cl_xml_view and sent to the browser with client->view_display( ). Data binding connects ABAP variables to UI5 controls:
client->_bind( var )— one-way binding (display only)client->_bind_edit( var )— two-way binding (user input is written back to the ABAP variable on the next roundtrip)
With that, you are ready for lesson 01.
Class: z2ui5_cl_tutorial_01 — Branch: lesson-01
The classic starting point: a form with two input fields and a button — the abap2UI5 equivalent of a SELECTION-SCREEN with PARAMETERS.
Everything starts with a plain class implementing z2ui5_if_app. The attributes you want to bind to the UI must be public, because the framework reads and writes them via RTTI:
CLASS z2ui5_cl_tutorial_01 DEFINITION PUBLIC.
PUBLIC SECTION.
INTERFACES z2ui5_if_app.
DATA product TYPE string.
DATA quantity TYPE string.On the first call (check_on_init), defaults are set and the view is built — a page containing a simple_form with labels, inputs and a button:
IF client->check_on_init( ).
product = `Notebook Basic 15`.
quantity = `10`.
DATA(view) = z2ui5_cl_xml_view=>factory( ).
view->shell(
)->page( title = `abap2UI5 Tutorial - 01 Selection Screen`
)->simple_form(
title = `Order Entry`
editable = abap_true
)->content( `form`
)->label( `Product`
)->input( client->_bind_edit( product )
)->label( `Quantity`
)->input( client->_bind_edit( quantity )
)->button(
text = `Post`
press = client->_event( `POST` ) ).
client->view_display( view->stringify( ) ).Two things carry the whole framework idea:
client->_bind_edit( product )— two-way binding. When the user changes the value in the browser, the framework writes it back into the ABAP attribute automatically on the next roundtrip. No event handling needed for the data transport.client->_event( 'POST' )— registers a server event for the button. When pressed,mainis called again and you handle it in the same method:
ELSEIF client->check_on_event( `POST` ).
client->message_toast_display( |{ quantity } x { product } - posted to the server| ).
ENDIF.The toast proves that the user input really arrived in the ABAP backend — without you writing a single line of transport code.
Key takeaway: bound attributes must be PUBLIC so the framework can read and write them via RTTI. State survives between roundtrips because the framework serializes and restores the whole app instance (draft persistence).
Class: z2ui5_cl_tutorial_02 — Branch: lesson-02
The ABAP developer's bread and butter: fill an internal table and display it — this time not with cl_salv_table, but with sap.m.Table.
1. A structure type and a public table attribute. Just like the scalar attributes in lesson 01, the table must be public to be bindable:
PUBLIC SECTION.
INTERFACES z2ui5_if_app.
TYPES:
BEGIN OF ty_s_product,
product_id TYPE string,
name TYPE string,
supplier TYPE string,
quantity TYPE i,
END OF ty_s_product.
DATA t_products TYPE STANDARD TABLE OF ty_s_product WITH EMPTY KEY.2. The canonical structure for larger apps. The app outgrows a single main method, so main becomes a pure dispatcher and the logic moves into dedicated methods. All following lessons keep this shape:
METHOD z2ui5_if_app~main.
me->client = client.
IF client->check_on_init( ).
on_init( ).
ENDIF.
ENDMETHOD.3. The table view. The table is bound with _bind — one-way is enough, the user does not change the data. columns( ) defines the headers, items( ) defines one template row:
DATA(tab) = view->shell(
)->page( title = `abap2UI5 Tutorial - 02 Internal Table`
)->table( client->_bind( t_products ) ).
tab->columns(
)->column(
)->text( `Product ID` )->get_parent(
)->column(
)->text( `Name` ).
" ...
tab->items(
)->column_list_item(
)->cells(
)->text( `{PRODUCT_ID}`
)->text( `{NAME}`
)->text( `{SUPPLIER}`
)->text( `{QUANTITY}` ).Binding paths like {PRODUCT_ID} refer to the components of the row structure — always uppercase.
Key takeaway: one column_list_item is a template — UI5 repeats it for every row of the bound table.
Class: z2ui5_cl_tutorial_03 — DDIC object: Z2UI5_T_TUTORIAL — Branch: lesson-03
Real applications read from the database. This lesson ships a transparent table Z2UI5_T_TUTORIAL (key field PRODUCT_ID, plus NAME, SUPPLIER, QUANTITY). The view stays identical to lesson 02 — only the data source changes.
1. The row type is now typed against the DDIC table instead of generic string components — the only change in the public section:
TYPES:
BEGIN OF ty_s_product,
product_id TYPE z2ui5_t_tutorial-product_id,
name TYPE z2ui5_t_tutorial-name,
supplier TYPE z2ui5_t_tutorial-supplier,
quantity TYPE z2ui5_t_tutorial-quantity,
END OF ty_s_product.2. data_seed fills the table with demo records on first use. MODIFY ... FROM TABLE is idempotent, so the lesson can be restarted any time:
METHOD data_seed.
SELECT COUNT( * ) FROM z2ui5_t_tutorial INTO @DATA(count).
IF count > 0.
RETURN.
ENDIF.
DATA t_db TYPE STANDARD TABLE OF z2ui5_t_tutorial WITH EMPTY KEY.
t_db = VALUE #(
( product_id = `HT-1000` name = `Notebook Basic 15` supplier = `SAP` quantity = 10 )
" ...
).
MODIFY z2ui5_t_tutorial FROM TABLE @t_db.
ENDMETHOD.3. data_read replaces the hard-coded VALUE #( ) constructor from lesson 02:
METHOD data_read.
SELECT FROM z2ui5_t_tutorial
FIELDS product_id, name, supplier, quantity
ORDER BY product_id
INTO TABLE @t_products.
ENDMETHOD.on_init now chains the steps — this separation (data in data_* methods, rendering in view_display) is the pattern all following lessons build on:
METHOD on_init.
data_seed( ).
data_read( ).
view_display( ).
ENDMETHOD.Key takeaway: the view does not care where the data comes from. Keep SELECTs in dedicated data_* methods.
Class: z2ui5_cl_tutorial_04 — Branch: lesson-04
A table the user cannot filter or sort is only half a table.
1. A public search attribute and a search bar above the table. The search field is two-way bound, so the search term is already in the ABAP attribute when the event arrives:
DATA search TYPE string.vbox->hbox(
)->search_field(
value = client->_bind_edit( search )
search = client->_event( `SEARCH` )
width = `17.5rem`
)->button(
icon = `sap-icon://sort-ascending`
press = client->_event( `SORT_ASCENDING` )
)->button(
icon = `sap-icon://sort-descending`
press = client->_event( `SORT_DESCENDING` ) ).2. An on_event method dispatching multiple events. With more than one event, main delegates to on_event and a CASE distinguishes them:
METHOD on_event.
CASE client->get( )-event.
WHEN `SEARCH`.
data_read( ).
WHEN `SORT_ASCENDING`.
SORT t_products BY name ASCENDING.
WHEN `SORT_DESCENDING`.
SORT t_products BY name DESCENDING.
ENDCASE.
client->view_model_update( ).
ENDMETHOD.3. The filter logic in data_read. After the SELECT, all rows that do not contain the search term in NAME or SUPPLIER are removed (case-insensitive via to_upper):
DATA(filter) = to_upper( search ).
LOOP AT t_products INTO DATA(s_product).
IF to_upper( s_product-name ) NS filter AND to_upper( s_product-supplier ) NS filter.
DELETE t_products.
ENDIF.
ENDLOOP.4. view_model_update instead of view_display. After each event only the JSON model is transferred to the browser — the view itself is unchanged and is not re-rendered. Faster, and the UI keeps its state (focus, scroll position).
Key takeaway: view_display renders a new view, view_model_update only refreshes the data of the current view. Use the cheapest one that does the job.
Class: z2ui5_cl_tutorial_05 — Branch: lesson-05
Now the table becomes interactive: the user selects a row and deletes it. (Search and sort from lesson 04 are left out here to keep the focus on selection — combining both is a good exercise.)
1. A selected component in the row structure. UI state like a row selection is just another bound field:
TYPES:
BEGIN OF ty_s_product,
selected TYPE abap_bool,
product_id TYPE z2ui5_t_tutorial-product_id,
" ...
END OF ty_s_product.2. The table becomes selectable and editable. mode = 'SingleSelectLeft' adds radio buttons, and the items are now bound with _bind_edit — that is what writes the selection state back to t_products on every roundtrip. The item template binds the new component:
)->table(
mode = `SingleSelectLeft`
items = client->_bind_edit( t_products ) ).
" ...
tab->items(
)->column_list_item( selected = `{SELECTED}`
)->cells( ...3. A header_toolbar with a title, a spacer and a Delete button:
tab->header_toolbar(
)->toolbar(
)->title( `Products`
)->toolbar_spacer(
)->button(
text = `Delete`
icon = `sap-icon://delete`
type = `Reject`
press = client->_event( `DELETE` ) ).4. The DELETE event handler. It finds the selected row in the internal table, guards against nothing being selected, deletes from the database and refreshes the model:
METHOD on_event_delete.
IF NOT line_exists( t_products[ selected = abap_true ] ).
client->message_toast_display( `Select an entry first` ).
RETURN.
ENDIF.
DATA(s_product) = t_products[ selected = abap_true ].
DELETE FROM z2ui5_t_tutorial WHERE product_id = @s_product-product_id.
data_read( ).
client->view_model_update( ).
client->message_toast_display( |{ s_product-name } deleted| ).
ENDMETHOD.Key takeaway: UI state like a row selection is just another bound field — read it from the internal table like any other value.
Class: z2ui5_cl_tutorial_06 — Branch: lesson-06
Deleting without asking is rude. abap2UI5 ships ready-made popups — here we use z2ui5_cl_pop_to_confirm.
1. The DELETE handler no longer deletes. Instead it opens the confirmation popup as a sub-app — the popup is pushed onto the app stack with nav_app_call and reports the user's choice back as a regular event:
METHOD on_event_delete.
IF NOT line_exists( t_products[ selected = abap_true ] ).
client->message_toast_display( `Select an entry first` ).
RETURN.
ENDIF.
DATA(s_product) = t_products[ selected = abap_true ].
client->nav_app_call( z2ui5_cl_pop_to_confirm=>factory(
i_question_text = |Delete { s_product-name }?|
i_event_confirm = `DELETE_CONFIRMED`
i_event_cancel = `DELETE_CANCELLED` ) ).
ENDMETHOD.2. Two new events in the dispatcher. Depending on the user's choice, our app receives DELETE_CONFIRMED or DELETE_CANCELLED — the actual DELETE FROM only runs in the confirmed branch:
CASE client->get( )-event.
WHEN `DELETE`.
on_event_delete( ).
WHEN `DELETE_CONFIRMED`.
on_event_delete_confirmed( ).
WHEN `DELETE_CANCELLED`.
client->message_toast_display( `Deletion cancelled` ).
ENDCASE.The view is completely unchanged compared to lesson 05 — the safety net is pure event flow.
Key takeaway: popups in abap2UI5 are just apps. nav_app_call pushes them on the app stack, and they report back via events.
Class: z2ui5_cl_tutorial_07 — Branch: lesson-07
The final lesson completes the app with an Add button and a custom popup form.
1. A public input structure for the new entry. The popup form binds against it:
DATA s_create TYPE ty_s_product.2. An Add button in the toolbar. Its handler resets the input structure and opens the popup:
WHEN `ADD`.
s_create = VALUE #( ).
popup_create_display( ).3. A custom dialog built with factory_popup. Same view builder, same binding as the main view — only the factory and the display call differ (popup_display instead of view_display):
METHOD popup_create_display.
DATA(popup) = z2ui5_cl_xml_view=>factory_popup( ).
DATA(dialog) = popup->dialog( `Create Product` ).
dialog->simple_form( editable = abap_true
)->content( `form`
)->label( `Product ID`
)->input( client->_bind_edit( s_create-product_id )
)->label( `Name`
)->input( client->_bind_edit( s_create-name )
" ...
).
dialog->buttons(
)->button(
text = `Cancel`
press = client->_event( `CREATE_CANCEL` )
)->button(
text = `Save`
press = client->_event( `CREATE_SAVE` )
type = `Emphasized` ).
client->popup_display( popup->stringify( ) ).
ENDMETHOD.4. Save and cancel handlers. CREATE_SAVE validates the input, maps the structure to the DDIC row with CORRESPONDING, persists it with MODIFY, closes the popup with popup_destroy and refreshes the table. CREATE_CANCEL simply closes the popup:
METHOD on_event_create_save.
IF s_create-product_id IS INITIAL OR s_create-name IS INITIAL.
client->message_toast_display( `Enter at least a product id and a name` ).
RETURN.
ENDIF.
DATA(s_db) = CORRESPONDING z2ui5_t_tutorial( s_create ).
MODIFY z2ui5_t_tutorial FROM @s_db.
client->popup_destroy( ).
data_read( ).
client->view_model_update( ).
client->message_toast_display( |{ s_create-name } created| ).
ENDMETHOD.Delete with confirmation from lesson 06 is still on board — the result is a small but complete maintenance app: list, create, delete with confirmation. This branch also ships the overview app z2ui5_cl_tutorial_00, which lists all lessons and navigates to them with nav_app_call.
Key takeaway: custom popups use the same view builder and the same binding as the main view — only the factory (factory_popup) and the display call (popup_display) differ.
- abap2UI5 samples — hundreds of further examples for nearly every UI5 control
- abap2UI5 documentation — guides, configuration, deployment scenarios
- UI5 API Reference — look up any control and its properties
Run the linter before committing:
npm install
npx abaplint