abap2UI5 Samples - Collection of demo apps for the abap2UI5 framework.
- This entire project is in English. All code, comments, commit messages, PR titles, PR descriptions, and any other text must be written in English.
- Run
abaplintbefore every commit. It must report 0 issues. - Configuration:
abaplint.jsonc - Install:
npm install -g @abaplint/cli - Run:
abaplint
All serialized files (.abap, .xml, and any other abapGit-managed file types) must conform to the abapGit file format:
- Encoding: UTF-8 (with optional BOM:
xEF BB BF) - Line endings: LF (
x0A) only — never CRLF - Final newline: every file must end with a single newline character after the last line
- Indentation: 2 spaces — never tabs
Always verify consistency for all file types before committing, not just .abap files. abaplint covers .abap files; for .xml and other files, check manually or via editor tooling that the above rules are met.
- Follow the SAP ABAP Style Guide.
- Never use an init flag attribute (
check_initialized,mv_init,is_initialized, etc.). Always useclient->check_on_init( )instead. - Use backticks for all string literals, not single quotes.
- Use string templates (
|...|with{ }for embedded expressions) instead of&&for string concatenation (e.g.|item { name }|not`item ` && name). - Prefer functional to procedural language constructs — use
var = VALUE #( ).to reset a variable, neverCLEAR var.. - Use type prefixes only for tables and structures: prefix table variables/attributes with
t_(e.g.t_items) and structure variables/attributes withs_(e.g.s_screen). Do not add prefixes to scalar variables or object references. - Name local types with a
ty_s_prefix for structure types (e.g.ty_s_row) andty_t_for table types (e.g.ty_t_rows). Only define aty_t_table type when it is used more than once — for a single-use table, declare it inline withSTANDARD TABLE OF ty_s_xxx. - No blank line between a
TYPESdefinition and theDATAdeclaration that directly uses it. - Class names are always written in lowercase in both
DEFINITIONandIMPLEMENTATION— never uppercase. - Classes are not
FINAL— do not add theFINALkeyword to class definitions. - Use
DEFINITION PUBLIC.— neverDEFINITION PUBLIC CREATE PUBLIC.(CREATE PUBLICis the default and adds unnecessary overhead). - Always include
PROTECTED SECTION.andPRIVATE SECTION.in the class definition, even if empty. - Keep
PRIVATE SECTION.always empty — declare everything atPROTECTED SECTION.level at most. - In every section (
PUBLIC SECTION.,PROTECTED SECTION.), always follow this declaration order:TYPESfirst, thenDATA, thenMETHODS. - Blank lines — class definition (
EMPTY_LINES_IN_CLASS_DEFINITION):- Add one blank line above each section keyword (
PUBLIC SECTION.,PROTECTED SECTION.,PRIVATE SECTION.) — unless the preceding section is empty. - No blank line directly below a section keyword.
- No blank line above
ENDCLASS.. - Max 1 consecutive blank line inside the definition block.
- Add one blank line between groups of different declaration types (e.g. between
INTERFACESandDATA, orDATAandMETHODS).
- Add one blank line above each section keyword (
- Blank lines — outside methods (
EMPTY_LINES_OUTSIDE_METHODS):- Exactly 2 blank lines between
ENDCLASS.and the nextCLASS … IMPLEMENTATION.(i.e. between the definition and the implementation block). - Exactly 2 blank lines between two
METHOD … ENDMETHOD.blocks. - Exactly 2 blank lines between two top-level class blocks.
- Exactly 2 blank lines between
- Blank lines — inside methods (
EMPTY_LINES_WITHIN_METHODS):- Always add exactly 1 blank line at the very start of a method body (after
METHOD). - Always add exactly 1 blank line at the very end of a method body (before
ENDMETHOD). - Max 1 consecutive blank line inside a method body.
- Always add 1 blank line before an
IFblock — except when the method is a pure dispatcher (its only purpose is to jump to other methods, with no own logic before theIF). In that case, omit the blank line between the opening assignment and theIF. - Always add 1 blank line before
ELSEIFandELSE. - In setup methods (
on_initand similar), add 1 blank line between the last data assignment and the first non-assignment statement (e.g. beforeview_display( )):METHOD on_init. price = `1234`. currency = `EUR`. view_display( ). ENDMETHOD.
- If a branch (
IF,ELSEIF,ELSE) contains more than one statement, add 1 blank line directly after the condition line as well:me->client = client. IF client->check_on_init( ). product = `products`. quantity = `500`. view_display( ). ELSEIF client->check_on_event( `SAVE` ). data_update( ). ENDIF.
- Always add exactly 1 blank line at the very start of a method body (after
- Always run
abaplintafter every change. It must report 0 issues before committing. - Before starting app development, read all active rules in
abaplint.jsoncand follow them throughout.
For deeper information about how the abap2UI5 framework works internally — architecture, roundtrip processing, data binding engine, session persistence, and core classes — refer to the abap2UI5 repository and its CLAUDE.md.
Every abap2UI5 app implements z2ui5_if_app with a single main() method. The framework calls main() on every roundtrip (HTTP POST). Use the lifecycle checks to react to different situations:
client->check_on_init( )— true on the very first callclient->check_on_navigated( )— true when returning from a sub-app or popupclient->check_on_event( )— true when a user triggered an event
Always use ELSEIF to chain these checks — never separate IF blocks:
IF client->check_on_init( ).
...
ELSEIF client->check_on_navigated( ).
...
ELSEIF client->check_on_event( ).
...
ENDIF.check_on_event( ) accepts an optional event name argument. Use it to check for a specific event directly in the ELSEIF chain when there are 2–3 events and no complex dispatch logic is needed:
IF client->check_on_init( ).
...
ELSEIF client->check_on_event( `SAVE` ).
data_update( ).
ELSEIF client->check_on_event( `DELETE` ).
data_delete( ).
ENDIF.Use a CASE statement (inside an ELSEIF client->check_on_event( ) block) only when there are 4 or more events, or when a dedicated on_event method is extracted for a larger app.
| Category | Methods | Purpose |
|---|---|---|
| Views | view_display, view_destroy, view_model_update |
Main view lifecycle |
| Nested views | nest_view_display/destroy/model_update, nest2_view_* |
Embedded sub-views |
| Popups | popup_display, popup_destroy, popup_model_update |
Modal dialogs |
| Popovers | popover_display, popover_destroy, popover_model_update |
Context popovers |
| Binding | _bind(val), _bind_edit(val) |
Read-only / two-way binding |
| Events | _event(val), _event_client(val), check_on_event(val) |
Event registration and checking |
| Navigation | nav_app_call(app), nav_app_leave(), get_app_prev() |
App stack navigation |
| Lifecycle | check_on_init(), check_on_navigated(), check_app_prev_stack() |
State checks |
| Messages | message_box_display(text), message_toast_display(text) |
User notifications |
| Session | set_session_stateful(val), set_app_state_active(val) |
Session management |
| Browser | set_push_state(val), set_nav_back(val), follow_up_action(val) |
Browser interaction |
| Info | get(), get_event_arg(), get_app(id) |
Request/context data |
| Constants | cs_event, cs_view |
Predefined event IDs and view names |
Always use client->_event_nav_app_leave() to bind the back button event directly in the view. This triggers navigation without a roundtrip to the ABAP backend:
METHOD view_display.
DATA(view) = z2ui5_cl_xml_view=>factory( ).
DATA(page) = view->shell(
)->page(
title = `My App`
shownavbutton = client->check_app_prev_stack( )
navbuttonpress = client->_event_nav_app_leave( ) ).
" ...
client->view_display( view->stringify( ) ).
ENDMETHOD.Only use the manual pattern (handling BACK in on_event) when you need to do something with the app or client instance before navigating back — for example, writing data back to the previous app:
METHOD on_event.
CASE client->get( )-event.
WHEN `BACK`.
" interact with previous app instance first
CAST z2ui5_cl_app_parent( client->get_app_prev( ) )->set_result( s_result ).
client->nav_app_leave( ).
ENDCASE.
ENDMETHOD.Views are XML strings passed to client->view_display(). There are two ways to build them:
Pre-built methods for common UI5 controls (shell, page, simple_form, input, button, etc.). Use this for standard layouts.
Always add 1 blank line before DATA(view) = z2ui5_cl_xml_view=>factory( ). to visually separate view construction from preceding logic.
Always build the view in view_display and call client->view_display( view->stringify( ) ) as a standalone statement at the end — never nested inside the chain.
Indent the fluent chain to reflect the XML hierarchy:
- Each method that navigates into a child element (returns a child node) is indented 4 spaces deeper than its parent call.
- Methods that add a sibling within the same container (and return the container) stay at the same indentation level.
- Single parameter: write inline —
)->label(Quantity)or)->input( client->_bind_edit( qty ) ). - More than one parameter: always split across multiple lines — one parameter per line, aligned below the opening
(, closing)on its own line:
)->input(
value = product
enabled = abap_false
)->button(
text = `Post`
press = client->_event( `POST` ) ).Never put two or more named parameters on the same line.
METHOD view_display.
DATA(view) = z2ui5_cl_xml_view=>factory( ).
view->shell(
)->page(
title = `My App`
navbuttonpress = client->_event_nav_app_leave( )
shownavbutton = client->check_app_prev_stack( )
)->simple_form(
title = `Form Title`
editable = abap_true
)->content( `form`
)->label( `Quantity`
)->input( client->_bind_edit( quantity )
)->label( `Product`
)->input(
value = product
enabled = abap_false
)->button(
text = `Post`
press = client->_event( `POST` ) ).
client->view_display( view->stringify( ) ).
ENDMETHOD.The hierarchy above is: shell → page → simple_form → content → (leaf elements).
label, input, button are siblings inside content, so they stay at the same indent level as )->content(.
Builds any XML structure directly from element names, namespaces and attributes. Look up the control in the UI5 API Reference and translate 1:1 to ABAP — no wrapper, no abstraction layer.
UI5 XML:
<form:SimpleForm title="T" editable="true">
<form:content>
<Label text="qty"/>
<Input value="{...}"/>
</form:content>
</form:SimpleForm>ABAP:
->_( n = `SimpleForm` ns = `form` p = VALUE #(
( n = `title` v = `T` )
( n = `editable` v = abap_true ) )
)->_( n = `content` ns = `form`
)->__( n = `Label` a = `text` v = `qty`
)->__( n = `Input` a = `value` v = client->_bind_edit( qty ) )Key rules for z2ui5_cl_util_xml:
_( n, ns, p )— add child element, navigate into it__( n, ns, a, v, p )— add child element, stay at current levelp( n, v )— add a single attribute to current element- Single attribute: use
a = ... v = ...parameters directly - Multiple attributes: use
p = VALUE #( ( n = ... v = ... ) ... ) - Namespace declarations go on the root
mvc:Viewelement as regular attributes (xmlns:form, etc.) - Only declare namespaces that are actually used in the view
stringify()on the factory root produces the complete XML string
Each call in a chain must start on its own line — never place two ->_() / ->__() calls on the same line:
" Wrong
DATA(page) = root->__( `Shell` )->__( n = `Page`
" Correct
DATA(page) = root->__( `Shell`
)->__( n = `Page`Chain continuations ()->) are indented 3 spaces relative to the statement's base indentation.
When p appears on a continuation line, align it directly under n — one space after the opening (:
" Wrong — p is one space too far right
)->_( n = `Input`
p = VALUE #( ... ) ).
" Correct — p directly under n
)->_( n = `Input`
p = VALUE #( ... ) ).Continuation lines inside VALUE #( ) align with the first tuple (:
)->_( n = `Input`
p = VALUE #( ( n = `type` v = `Number` )
( n = `value` v = client->_bind_edit( qty ) ) ) ).In VALUE #( ) constructor expressions with named fields, use either entirely inline (all fields on one line) or each field on its own line — never mix both styles in the same expression:
" Wrong — mixed
t_products = VALUE #(
( name = `Notebook Basic 15` product_id = `HT-1000` supplier_name = `Very Best Screens`
dimensions = `30 x 18 x 3 cm` weight_measure = `4.2` weight_unit = `KG` ) ).
" Correct — one field per line, = signs aligned
t_products = VALUE #(
( name = `Notebook Basic 15`
product_id = `HT-1000`
supplier_name = `Very Best Screens`
dimensions = `30 x 18 x 3 cm`
weight_measure = `4.2`
weight_unit = `KG` ) ).Write everything directly in main — no method encapsulation needed. Count only the lines inside the main method, not the total class length.
Do not extract view_display or any other helper method just because the app has a view. A separate view_display method is only justified when the app is large enough to warrant the full canonical structure (≥ 50 lines in main). Extracting it in a simple app adds unnecessary indirection.
When the logic no longer fits inside main, always extract exactly on_init and on_event as the first step — never use other method names for this purpose. main then becomes a pure dispatcher that calls these two methods. Only add further methods (view_display, data_read, etc.) when they are actually needed.
Never create a pass-through method with only one statement. If an extracted method (e.g. on_init) would contain only a single call, replace the method call in the dispatcher with that single call directly — and omit the pass-through method entirely. For example, if on_init would only call view_display( ), write view_display( ) directly in the IF client->check_on_init( ). branch instead.
The following is the maximum structure. Only add methods that are actually needed.
When the body of a single WHEN branch in on_event grows too long, extract it into a dedicated method named on_event_<event> (e.g. on_event_save, on_event_delete). The on_event method then stays a thin dispatcher — one call per branch — and all the logic lives in the sub-method.
CLASS z2ui5_cl_app_xxx DEFINITION PUBLIC.
PUBLIC SECTION.
INTERFACES z2ui5_if_app.
" bound data (DATA attributes for _bind/_bind_edit)...
PROTECTED SECTION.
DATA client TYPE REF TO z2ui5_if_client.
METHODS on_init. " first call: load data, display view
METHODS on_event. " user triggered an event
METHODS on_navigation. " returned from sub-app or popup
METHODS view_display. " build and render the view
METHODS data_read. " SELECT from database
METHODS data_update. " INSERT / UPDATE / DELETE
PRIVATE SECTION.
ENDCLASS.
CLASS z2ui5_cl_app_xxx IMPLEMENTATION.
METHOD z2ui5_if_app~main.
me->client = client.
IF client->check_on_init( ).
on_init( ).
ELSEIF client->check_on_navigated( ).
on_navigation( ).
ELSEIF client->check_on_event( ).
on_event( ).
ENDIF.
ENDMETHOD.
METHOD on_init.
data_read( ).
view_display( ).
ENDMETHOD.
METHOD on_navigation.
data_read( ).
client->view_model_update( ).
ENDMETHOD.
METHOD on_event.
CASE client->get( )-event.
WHEN `SAVE`.
on_event_save( ).
WHEN `BACK`.
client->nav_app_leave( ).
ENDCASE.
ENDMETHOD.
METHOD on_event_save.
data_update( ).
ENDMETHOD.
METHOD view_display.
DATA(view) = z2ui5_cl_xml_view=>factory( ).
" ...
client->view_display( view->stringify( ) ).
ENDMETHOD.
METHOD data_read.
" SELECT ...
ENDMETHOD.
METHOD data_update.
" INSERT / UPDATE / DELETE ...
ENDMETHOD.
ENDCLASS.