diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index aa0861a2..1fde2bc1 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -123,6 +123,7 @@ export default defineConfig({ // The section is called Getting Started in the sidebar this opens; // "Introduction" was the first PAGE in it, one level down. { text: "Getting Started", link: "/get_started/about" }, // nav + { text: "Tutorials", link: "/tutorials/overview" }, // nav { text: "Cookbook", link: "/cookbook/overview" }, { text: "Configuration", link: "/configuration/setup" }, // Technical Insight is not a line of its own here any more: it is a @@ -192,6 +193,33 @@ export default defineConfig({ { text: `What's Next?`, link: "/get_started/next" }, ], }, + { + // Between Getting Started and the Cookbook on purpose: a tutorial is + // read after the framework is installed and before the reference + // chapters, and that is the order the three sections stand in. + text: "Tutorials", + link: "/tutorials/overview", + collapsed: true, + items: [ + { text: "Overview", link: "/tutorials/overview" }, // sidebar + { + text: "Walkthrough", + link: "/tutorials/walkthrough/overview", + collapsed: true, + items: [ + { text: "Introduction", link: "/tutorials/walkthrough/overview" }, // sidebar + { text: "1 — Hello World", link: "/tutorials/walkthrough/step-1" }, + { text: "2 — A First View", link: "/tutorials/walkthrough/step-2" }, + { text: "3 — Events", link: "/tutorials/walkthrough/step-3" }, + { text: "4 — Data Binding", link: "/tutorials/walkthrough/step-4" }, + { text: "5 — List Binding", link: "/tutorials/walkthrough/step-5" }, + { text: "6 — Row Events", link: "/tutorials/walkthrough/step-6" }, + { text: "7 — Popups", link: "/tutorials/walkthrough/step-7" }, + { text: "8 — App Structure", link: "/tutorials/walkthrough/step-8" }, + ], + }, + ], + }, { text: "Cookbook", link: "/cookbook/overview", diff --git a/docs/public/tutorials/walkthrough-preview.png b/docs/public/tutorials/walkthrough-preview.png new file mode 100644 index 00000000..eb2f4d91 Binary files /dev/null and b/docs/public/tutorials/walkthrough-preview.png differ diff --git a/docs/tutorials/overview.md b/docs/tutorials/overview.md new file mode 100644 index 00000000..ba43f701 --- /dev/null +++ b/docs/tutorials/overview.md @@ -0,0 +1,32 @@ +--- +outline: [2, 4] +description: Learn abap2UI5 by building — step-by-step tutorials where every step is a complete class you can run in the browser. +--- +# Tutorials + +Learn abap2UI5 by building something. A tutorial is a sequence of steps, and +every step is one **complete class**: run it directly under its code with the +Run button, or copy it into your system and start it like any abap2UI5 app +(see the [Quickstart](/get_started/quickstart)). + +## Learning Path + +| Tutorial | Type | Content | +|---|---|---| +| [Walkthrough](/tutorials/walkthrough/overview) | Foundation | Builds a small invoice app from scratch, one step at a time — the app class and its lifecycle, views built in ABAP, events, data binding, lists, popups, and the structure real apps use. | + +More tutorials — deep dives into single topics — will follow. Until then, the +[Cookbook](/cookbook/overview) covers each topic as a reference chapter. + +## After the Tutorials + +Three catalogues of complete, tested apps continue where the tutorials stop: + +- [Samples](https://abap2ui5.github.io/samples/) — one app per pattern, along a guided learning path +- [Controls](https://abap2ui5.github.io/samples-controls/) — UI5 demo kit samples rebuilt as abap2UI5 apps, searchable by control +- [Stack](https://abap2ui5.github.io/samples-stack/) — integration samples per technology, from RAP to WebSocket + +And two pages of this site compress the walkthrough for readers in a hurry: +[Hello World](/get_started/hello_world) covers the first steps on a single +page, the [Full Example](/get_started/full_example) builds a realistic +selection-screen app on top of them. diff --git a/docs/tutorials/walkthrough/overview.md b/docs/tutorials/walkthrough/overview.md new file mode 100644 index 00000000..e4e20302 --- /dev/null +++ b/docs/tutorials/walkthrough/overview.md @@ -0,0 +1,42 @@ +--- +outline: [2, 4] +description: Build a small invoice app from scratch in eight steps — every step a complete, runnable abap2UI5 class. +--- +# Walkthrough + +In this tutorial we build a small invoice app from scratch and meet every +paradigm an abap2UI5 app is made of along the way: the app class and its +lifecycle, views written in ABAP, events, data binding, lists and popups. + +## Preview + +![The finished walkthrough app: a list of invoices with supplier and quantity, and a dialog editing the quantity of one row](/tutorials/walkthrough-preview.png) + +This is the app after the last step — about a hundred lines of ABAP, no +frontend project, no OData service. + +Each step is a **complete, runnable class** — the whole app as it stands at +that point, not a fragment. Press the Run button under the code to start it in +the browser, or copy the class into your system and launch it like any +abap2UI5 app (see the [Quickstart](/get_started/quickstart)). No step depends +on anything outside its own code, so you can also jump straight to the step +you are interested in. + +## Steps + +- **[Step 1: Hello World](/tutorials/walkthrough/step-1)** — the smallest possible app: one class, one method, one message. +- **[Step 2: A First View](/tutorials/walkthrough/step-2)** — render a UI5 view built entirely in ABAP. +- **[Step 3: Events](/tutorials/walkthrough/step-3)** — a button, a press event, and the lifecycle behind them. +- **[Step 4: Data Binding](/tutorials/walkthrough/step-4)** — an input field whose value reaches the server by itself. +- **[Step 5: List Binding](/tutorials/walkthrough/step-5)** — show an internal table as a UI5 list. +- **[Step 6: Row Events](/tutorials/walkthrough/step-6)** — react to a click on a row, and know which row it was. +- **[Step 7: Popups](/tutorials/walkthrough/step-7)** — edit a row in a dialog. +- **[Step 8: App Structure](/tutorials/walkthrough/step-8)** — refactor into the structure real apps use. + +## What You Should Know + +No prior abap2UI5 or UI5 experience is needed — the tutorial introduces every +concept as it appears. Basic ABAP (classes, methods, internal tables) is +assumed. If you want the framework installed in your own system first, do the +[Quickstart](/get_started/quickstart) — but the Run button works without any +installation at all. diff --git a/docs/tutorials/walkthrough/step-1.md b/docs/tutorials/walkthrough/step-1.md new file mode 100644 index 00000000..505340e6 --- /dev/null +++ b/docs/tutorials/walkthrough/step-1.md @@ -0,0 +1,43 @@ +--- +outline: [2, 4] +description: The smallest possible abap2UI5 app — one class, one method, one message. +--- +# Step 1: Hello World + +Every abap2UI5 app is one ABAP class implementing the interface +`z2ui5_if_app`. That interface has a single method, `main`, and the framework +calls it with one parameter: `client`, your only API. This is the smallest app +that can exist: + +```abap +CLASS zcl_app_walkthrough DEFINITION PUBLIC. + PUBLIC SECTION. + INTERFACES z2ui5_if_app. +ENDCLASS. + +CLASS zcl_app_walkthrough IMPLEMENTATION. + METHOD z2ui5_if_app~main. + client->message_box_display( `Hello World` ). + ENDMETHOD. +ENDCLASS. +``` + +Press **Run this example** under the code — the class starts right here in +the browser. To run it in your own system instead, copy it in, open the +abap2UI5 startup page (see the [Quickstart](/get_started/quickstart)), and +enter the class name. + +## What Just Happened + +- **No app project, no OData service, no frontend artifact.** The class *is* + the app. abap2UI5 follows a thin-frontend model: the browser only renders, + while all logic, state and data stay in ABAP on the server. +- **`main` runs on every roundtrip.** The framework calls it when the app + starts and again after every user interaction. Right now every call shows + the same message box; from [Step 3](/tutorials/walkthrough/step-3) on we + will tell the calls apart. +- **`client` is the whole API.** Displaying views and messages, reacting to + events, binding data — everything in this tutorial goes through this one + object. + +Next, we replace the message box with a real UI5 view. diff --git a/docs/tutorials/walkthrough/step-2.md b/docs/tutorials/walkthrough/step-2.md new file mode 100644 index 00000000..0888e5d0 --- /dev/null +++ b/docs/tutorials/walkthrough/step-2.md @@ -0,0 +1,62 @@ +--- +outline: [2, 4] +description: Render a UI5 XML view built entirely in ABAP with the view builder. +--- +# Step 2: A First View + +A message box is not much of a UI. UI5 apps describe their screens as XML +views — and in abap2UI5 you write that view in ABAP, with +`z2ui5_cl_ui5_view_builder`: + +```abap +CLASS zcl_app_walkthrough DEFINITION PUBLIC. + PUBLIC SECTION. + INTERFACES z2ui5_if_app. +ENDCLASS. + +CLASS zcl_app_walkthrough IMPLEMENTATION. + METHOD z2ui5_if_app~main. + + 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 = `Walkthrough - Step 2` + + )->tag( `Text` + )->a( n = `text` v = `Hello World` ). + + client->view_display( view->stringify( ) ). + + ENDMETHOD. +ENDCLASS. +``` + +## The Four Verbs + +You are writing a UI5 XML view, one control per call. The builder has four +verbs and no list of controls to look up — every UI5 control, property and +aggregation is available, because the builder never knew any of them by name: + +| | | +| --- | --- | +| `ele( )` | add a control and **descend** into it — for a container | +| `tag( )` | add a control and **stay** — for a leaf | +| `a( )` | set **one** attribute on the control the chain points at | +| `end( )` | ascend to the parent | + +The single rule: `a( )` applies to the control the chain currently points at, +so attributes follow their control — and a control gets them *before* its +first child. The root `mvc:View` and its `xmlns` declarations are written by +hand, exactly as in a real UI5 view. `stringify( )` renders the XML from the +root, and `view_display( )` sends it to the browser. + +The indentation mirrors the XML tree — `Text` sits inside `Page` inside +`Shell` — which is what makes the chain readable as the view it builds. The +full layout rules are on the [View → Definition](/cookbook/view/definition) +page. + +Next, the app gets its first button — and with it, the app lifecycle. diff --git a/docs/tutorials/walkthrough/step-3.md b/docs/tutorials/walkthrough/step-3.md new file mode 100644 index 00000000..c6d1f947 --- /dev/null +++ b/docs/tutorials/walkthrough/step-3.md @@ -0,0 +1,77 @@ +--- +outline: [2, 4] +description: A button, a press event, and the abap2UI5 lifecycle behind them. +--- +# Step 3: Events + +The framework calls `main` on **every** roundtrip — on the initial start and +again after each user interaction. As soon as an app reacts to input, `main` +has to tell those calls apart. That job falls to the lifecycle checks, and +`main` becomes a dispatcher: + +```abap +CLASS zcl_app_walkthrough DEFINITION PUBLIC. + PUBLIC SECTION. + INTERFACES z2ui5_if_app. +ENDCLASS. + +CLASS zcl_app_walkthrough IMPLEMENTATION. + METHOD z2ui5_if_app~main. + + IF client->check_on_navigated( ). + + 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 = `Walkthrough - Step 3` + + )->tag( `Text` + )->a( n = `text` v = `Hello World` + )->tag( `Button` + )->a( n = `text` v = `Say Hello` + )->a( n = `press` v = client->_event( `SAY_HELLO` ) ). + + client->view_display( view->stringify( ) ). + + ELSEIF client->check_on_event( `SAY_HELLO` ). + + client->message_toast_display( `Hello from abap2UI5!` ). + + ENDIF. + + ENDMETHOD. +ENDCLASS. +``` + +## The Lifecycle + +```text +┌──────────┐ ┌──────────┐ ┌──────────┐ +│ Browser │──────>│ main() │──────>│ Browser │ +│ (Start) │ HTTP │ view │ HTTP │ (View) │ +└──────────┘ └──────────┘ └────┬─────┘ + │ user clicks +┌──────────┐ ┌──────────┐ ┌────┴─────┐ +│ Browser │<──────│ main() │<──────│ Browser │ +│ (Toast) │ HTTP │ event │ HTTP │ (Event) │ +└──────────┘ └──────────┘ └──────────┘ +``` + +- **`check_on_navigated( )`** is true when the app has to draw its screen — + on the first start, and again whenever the user navigates back to it later. + This branch displays the view. +- **`check_on_event( )`** is true when the user triggered an event. The + argument names which one. +- ``client->_event( `SAY_HELLO` )`` wires the button: it returns the press + handler that sends the event — with the name you chose — back to `main`. + +Each `check_*` method is true only for its own phase, so the `IF`/`ELSEIF` +chain cleanly dispatches every roundtrip. The full picture is on the +[Life Cycle](/cookbook/event_navigation/life_cycle) page. + +Next: data leaves the browser and reaches your class — without a single line +of transfer code. diff --git a/docs/tutorials/walkthrough/step-4.md b/docs/tutorials/walkthrough/step-4.md new file mode 100644 index 00000000..876cc83c --- /dev/null +++ b/docs/tutorials/walkthrough/step-4.md @@ -0,0 +1,67 @@ +--- +outline: [2, 4] +description: Bind an input field to a class attribute — data reaches the server by itself. +--- +# Step 4: Data Binding + +So far the app only talks. Now the user answers: an input field replaces the +text, and whatever is typed into it arrives in your class — without a single +line of transfer code: + +```abap +CLASS zcl_app_walkthrough DEFINITION PUBLIC. + PUBLIC SECTION. + INTERFACES z2ui5_if_app. + DATA recipient TYPE string. +ENDCLASS. + +CLASS zcl_app_walkthrough IMPLEMENTATION. + METHOD z2ui5_if_app~main. + + IF client->check_on_navigated( ). + + recipient = `World`. + + 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 = `Walkthrough - Step 4` + + )->tag( `Input` + )->a( n = `value` v = client->_bind( recipient ) + )->tag( `Button` + )->a( n = `text` v = `Say Hello` + )->a( n = `press` v = client->_event( `SAY_HELLO` ) ). + + client->view_display( view->stringify( ) ). + + ELSEIF client->check_on_event( `SAY_HELLO` ). + + client->message_toast_display( |Hello { recipient }!| ). + + ENDIF. + + ENDMETHOD. +ENDCLASS. +``` + +## How the Value Travels + +- **`DATA recipient TYPE string`** — a public attribute is the model. The + framework serializes public attributes after every roundtrip and restores + them before the next one, so your class keeps its state without any session + handling. The attribute must be in the `PUBLIC SECTION`: the framework + reads it dynamically and silently ignores private or protected ones (full + rules on the [Binding](/cookbook/model/binding) page). +- **`client->_bind( recipient )`** connects the attribute to the `value` + property of the input. When the button fires, the browser sends the current + screen state along with the event — by the time your `ELSEIF` branch runs, + `recipient` already holds what the user typed. +- **Type it, press the button** — the toast greets whatever name is in the + field. Two-way, and you wrote no transfer code. + +Next, the same binding moves a whole internal table into a list. diff --git a/docs/tutorials/walkthrough/step-5.md b/docs/tutorials/walkthrough/step-5.md new file mode 100644 index 00000000..dd86327c --- /dev/null +++ b/docs/tutorials/walkthrough/step-5.md @@ -0,0 +1,82 @@ +--- +outline: [2, 4] +description: Show an internal table as a UI5 list with aggregation binding. +--- +# Step 5: List Binding + +An app that greets is nice; an app that shows data is useful. The invoice +app starts here: an internal table of invoices, bound to a UI5 list — +one row template, repeated per line of the table: + +```abap +CLASS zcl_app_walkthrough DEFINITION PUBLIC. + PUBLIC SECTION. + INTERFACES z2ui5_if_app. + + TYPES: + BEGIN OF ty_s_invoice, + product TYPE string, + supplier TYPE string, + quantity TYPE string, + END OF ty_s_invoice. + + DATA t_invoices TYPE STANDARD TABLE OF ty_s_invoice WITH EMPTY KEY. +ENDCLASS. + +CLASS zcl_app_walkthrough IMPLEMENTATION. + METHOD z2ui5_if_app~main. + + IF client->check_on_navigated( ). + + t_invoices = VALUE #( + ( product = `Pineapple` supplier = `ACME` quantity = `21` ) + ( product = `Milk` supplier = `Green Growers` quantity = `4` ) + ( product = `Canned Beans` supplier = `Corner Deli` quantity = `3` ) + ( product = `Salad` supplier = `Green Growers` quantity = `2` ) + ( product = `Bread` supplier = `Corner Deli` quantity = `1` ) ). + + 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 = `Walkthrough - Step 5` + + )->ele( `List` + )->a( n = `headerText` v = `Invoices` + )->a( n = `items` v = client->_bind( t_invoices ) + + )->ele( `items` + )->tag( `StandardListItem` + )->a( n = `title` v = `{PRODUCT}` + )->a( n = `description` v = `{SUPPLIER}` + )->a( n = `info` v = `{QUANTITY}` ). + + client->view_display( view->stringify( ) ). + + ENDIF. + + ENDMETHOD. +ENDCLASS. +``` + +## One Template, Many Rows + +- **The table binds like the string did.** ``client->_bind( t_invoices )`` + on the list's `items` aggregation — the same call as in + [Step 4](/tutorials/walkthrough/step-4), just with an internal table behind + it. +- **The `StandardListItem` is a template, not a row.** UI5 clones it once per + line of the table. Inside the template, `{PRODUCT}` is a plain UI5 binding + path relative to the row — the framework maps your ABAP field names, in + uppercase. +- **The data is initialized in the `check_on_navigated` branch**, right + before the view. In a real app this is where your `SELECT` runs; the + tutorial keeps demo data so the class stays self-contained. + +Tables (`sap.m.Table` with columns and cells) bind exactly the same way — +the [Tables](/cookbook/model/tables) page has that variant. + +Next, the rows learn to react to a click. diff --git a/docs/tutorials/walkthrough/step-6.md b/docs/tutorials/walkthrough/step-6.md new file mode 100644 index 00000000..9d32b013 --- /dev/null +++ b/docs/tutorials/walkthrough/step-6.md @@ -0,0 +1,92 @@ +--- +outline: [2, 4] +description: React to a click on a list row and know which row it was — events with arguments. +--- +# Step 6: Row Events + +A list of five rows raises a question [Step 3](/tutorials/walkthrough/step-3) +did not have: *which* row was clicked? The event stays the same — what is new +is the argument it carries: + +```abap +CLASS zcl_app_walkthrough DEFINITION PUBLIC. + PUBLIC SECTION. + INTERFACES z2ui5_if_app. + + TYPES: + BEGIN OF ty_s_invoice, + product TYPE string, + supplier TYPE string, + quantity TYPE string, + END OF ty_s_invoice. + + DATA t_invoices TYPE STANDARD TABLE OF ty_s_invoice WITH EMPTY KEY. +ENDCLASS. + +CLASS zcl_app_walkthrough IMPLEMENTATION. + METHOD z2ui5_if_app~main. + + IF client->check_on_navigated( ). + + t_invoices = VALUE #( + ( product = `Pineapple` supplier = `ACME` quantity = `21` ) + ( product = `Milk` supplier = `Green Growers` quantity = `4` ) + ( product = `Canned Beans` supplier = `Corner Deli` quantity = `3` ) + ( product = `Salad` supplier = `Green Growers` quantity = `2` ) + ( product = `Bread` supplier = `Corner Deli` quantity = `1` ) ). + + 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 = `Walkthrough - Step 6` + + )->ele( `List` + )->a( n = `headerText` v = `Invoices` + )->a( n = `items` v = client->_bind( t_invoices ) + + )->ele( `items` + )->tag( `StandardListItem` + )->a( n = `title` v = `{PRODUCT}` + )->a( n = `description` v = `{SUPPLIER}` + )->a( n = `info` v = `{QUANTITY}` + )->a( n = `type` v = `Active` + )->a( n = `press` v = client->_event( val = `SHOW_INVOICE` + t_arg = VALUE #( ( `${PRODUCT}` ) ) ) ). + + client->view_display( view->stringify( ) ). + + ELSEIF client->check_on_event( `SHOW_INVOICE` ). + + DATA(s_invoice) = VALUE #( t_invoices[ product = client->get_event_arg( ) ] OPTIONAL ). + client->message_toast_display( |{ s_invoice-quantity } x { s_invoice-product } from { s_invoice-supplier }| ). + + ENDIF. + + ENDMETHOD. +ENDCLASS. +``` + +## Events With Arguments + +- **`type = Active`** makes the whole row clickable and gives it press + feedback; the `press` handler sits on the row template, so every row fires + the same event. +- **The argument rides with the event:** + ``t_arg = VALUE #( ( `${PRODUCT}` ) )``. The `${...}` syntax is resolved + *per row* in the browser — each clone of the template carries its own + product name. On the server, `client->get_event_arg( )` returns it, and a + table read finds the row. +- **The state is still there.** `t_invoices` was filled in the + `check_on_navigated` branch of an earlier roundtrip — the framework + restored it before this one, so the event handler can read it. That is the + serialization from [Step 4](/tutorials/walkthrough/step-4) doing its job. + +More on both event directions — server events like this one, and events +handled purely in the browser — under +[Event → Backend](/cookbook/event_navigation/backend). + +A toast is a modest way to show an invoice. Next, a real dialog. diff --git a/docs/tutorials/walkthrough/step-7.md b/docs/tutorials/walkthrough/step-7.md new file mode 100644 index 00000000..1fc44130 --- /dev/null +++ b/docs/tutorials/walkthrough/step-7.md @@ -0,0 +1,130 @@ +--- +outline: [2, 4] +description: Edit a list row in a dialog — popups with the same builder, displayed over the running view. +--- +# Step 7: Popups + +Clicking a row now opens a dialog to edit the invoice's quantity. A popup is +built with the same builder and the same verbs as the view — only the root +element changes (`core:FragmentDefinition` instead of `mvc:View`), and it is +handed to `popup_display( )` while the main view stays untouched behind it: + +```abap +CLASS zcl_app_walkthrough DEFINITION PUBLIC. + PUBLIC SECTION. + INTERFACES z2ui5_if_app. + + TYPES: + BEGIN OF ty_s_invoice, + product TYPE string, + supplier TYPE string, + quantity TYPE string, + END OF ty_s_invoice. + + DATA t_invoices TYPE STANDARD TABLE OF ty_s_invoice WITH EMPTY KEY. + DATA s_edit TYPE ty_s_invoice. +ENDCLASS. + +CLASS zcl_app_walkthrough IMPLEMENTATION. + METHOD z2ui5_if_app~main. + + IF client->check_on_navigated( ). + + t_invoices = VALUE #( + ( product = `Pineapple` supplier = `ACME` quantity = `21` ) + ( product = `Milk` supplier = `Green Growers` quantity = `4` ) + ( product = `Canned Beans` supplier = `Corner Deli` quantity = `3` ) + ( product = `Salad` supplier = `Green Growers` quantity = `2` ) + ( product = `Bread` supplier = `Corner Deli` quantity = `1` ) ). + + 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 = `Walkthrough - Step 7` + + )->ele( `List` + )->a( n = `headerText` v = `Invoices` + )->a( n = `items` v = client->_bind( t_invoices ) + + )->ele( `items` + )->tag( `StandardListItem` + )->a( n = `title` v = `{PRODUCT}` + )->a( n = `description` v = `{SUPPLIER}` + )->a( n = `info` v = `{QUANTITY}` + )->a( n = `type` v = `Active` + )->a( n = `press` v = client->_event( val = `EDIT` + t_arg = VALUE #( ( `${PRODUCT}` ) ) ) ). + + client->view_display( view->stringify( ) ). + + ELSEIF client->check_on_event( `EDIT` ). + + s_edit = VALUE #( t_invoices[ product = client->get_event_arg( ) ] OPTIONAL ). + + DATA(popup) = z2ui5_cl_ui5_view_builder=>factory( + )->ele( n = `FragmentDefinition` ns = `core` + )->a( n = `xmlns` v = `sap.m` + )->a( n = `xmlns:core` v = `sap.ui.core` + + )->ele( `Dialog` + )->a( n = `title` v = |Edit { s_edit-product }| + + )->ele( `content` + + )->tag( `Label` + )->a( n = `text` v = `Quantity` + )->tag( `Input` + )->a( n = `value` v = client->_bind( s_edit-quantity ) + + )->end( + + )->ele( `buttons` + + )->tag( `Button` + )->a( n = `text` v = `Cancel` + )->a( n = `press` v = client->_event( `CANCEL` ) + )->tag( `Button` + )->a( n = `text` v = `Save` + )->a( n = `press` v = client->_event( `SAVE` ) + )->a( n = `type` v = `Emphasized` ). + + client->popup_display( popup->stringify( ) ). + + ELSEIF client->check_on_event( `SAVE` ). + + t_invoices[ product = s_edit-product ]-quantity = s_edit-quantity. + client->popup_destroy( ). + client->message_toast_display( |{ s_edit-product } updated.| ). + + ELSEIF client->check_on_event( `CANCEL` ). + + client->popup_destroy( ). + + ENDIF. + + ENDMETHOD. +ENDCLASS. +``` + +## What Is New + +- **`s_edit` is the popup's model.** The `EDIT` handler copies the clicked + row into it, and the dialog's input binds to `s_edit-quantity` — by the + time `SAVE` arrives, the attribute already holds the new value. +- **`popup_display( )` / `popup_destroy( )`** show and close the dialog. The + main view is never rebuilt — it waits behind the popup. +- **No refresh call after `SAVE`.** The handler changes `t_invoices` and + does nothing else: every roundtrip that changed bound data pushes the new + model to the browser by itself, and the list updates. (You may see + `view_model_update( )` in older code — it is deliberately empty and does + nothing.) + +Popovers, message boxes and the built-in popup classes are covered under +[Popup, Popover](/cookbook/popup_popover/popup). + +The app is complete — one last step separates it into the structure real +apps use. diff --git a/docs/tutorials/walkthrough/step-8.md b/docs/tutorials/walkthrough/step-8.md new file mode 100644 index 00000000..3e8d6eb5 --- /dev/null +++ b/docs/tutorials/walkthrough/step-8.md @@ -0,0 +1,164 @@ +--- +outline: [2, 4] +description: Refactor the walkthrough app into the structure real apps use — a dispatcher, named methods, protected state. +--- +# Step 8: App Structure + +The app is complete, but everything lives in one `main` method. Real apps — +the framework's own, and the sample catalogues' — separate the phases into +methods, so this last step changes no behavior at all: it puts the code where +a reader expects it. + +```abap +CLASS zcl_app_walkthrough DEFINITION PUBLIC. + + PUBLIC SECTION. + INTERFACES z2ui5_if_app. + + TYPES: + BEGIN OF ty_s_invoice, + product TYPE string, + supplier TYPE string, + quantity TYPE string, + END OF ty_s_invoice. + + DATA t_invoices TYPE STANDARD TABLE OF ty_s_invoice WITH EMPTY KEY. + DATA s_edit TYPE ty_s_invoice. + + PROTECTED SECTION. + DATA client TYPE REF TO z2ui5_if_client. + + METHODS on_event. + METHODS view_display. + METHODS popup_edit_display. + +ENDCLASS. + +CLASS zcl_app_walkthrough IMPLEMENTATION. + + METHOD z2ui5_if_app~main. + + me->client = client. + IF client->check_on_navigated( ). + view_display( ). + ELSEIF client->check_on_event( ). + on_event( ). + ENDIF. + + ENDMETHOD. + + + METHOD on_event. + + CASE client->get( )-event. + WHEN `EDIT`. + s_edit = VALUE #( t_invoices[ product = client->get_event_arg( ) ] OPTIONAL ). + popup_edit_display( ). + WHEN `SAVE`. + t_invoices[ product = s_edit-product ]-quantity = s_edit-quantity. + client->popup_destroy( ). + client->message_toast_display( |{ s_edit-product } updated.| ). + WHEN `CANCEL`. + client->popup_destroy( ). + ENDCASE. + + ENDMETHOD. + + + METHOD view_display. + + t_invoices = VALUE #( + ( product = `Pineapple` supplier = `ACME` quantity = `21` ) + ( product = `Milk` supplier = `Green Growers` quantity = `4` ) + ( product = `Canned Beans` supplier = `Corner Deli` quantity = `3` ) + ( product = `Salad` supplier = `Green Growers` quantity = `2` ) + ( product = `Bread` supplier = `Corner Deli` quantity = `1` ) ). + + 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 = `Walkthrough - Step 8` + + )->ele( `List` + )->a( n = `headerText` v = `Invoices` + )->a( n = `items` v = client->_bind( t_invoices ) + + )->ele( `items` + )->tag( `StandardListItem` + )->a( n = `title` v = `{PRODUCT}` + )->a( n = `description` v = `{SUPPLIER}` + )->a( n = `info` v = `{QUANTITY}` + )->a( n = `type` v = `Active` + )->a( n = `press` v = client->_event( val = `EDIT` + t_arg = VALUE #( ( `${PRODUCT}` ) ) ) ). + + client->view_display( view->stringify( ) ). + + ENDMETHOD. + + + METHOD popup_edit_display. + + DATA(popup) = z2ui5_cl_ui5_view_builder=>factory( + )->ele( n = `FragmentDefinition` ns = `core` + )->a( n = `xmlns` v = `sap.m` + )->a( n = `xmlns:core` v = `sap.ui.core` + + )->ele( `Dialog` + )->a( n = `title` v = |Edit { s_edit-product }| + + )->ele( `content` + + )->tag( `Label` + )->a( n = `text` v = `Quantity` + )->tag( `Input` + )->a( n = `value` v = client->_bind( s_edit-quantity ) + + )->end( + + )->ele( `buttons` + + )->tag( `Button` + )->a( n = `text` v = `Cancel` + )->a( n = `press` v = client->_event( `CANCEL` ) + )->tag( `Button` + )->a( n = `text` v = `Save` + )->a( n = `press` v = client->_event( `SAVE` ) + )->a( n = `type` v = `Emphasized` ). + + client->popup_display( popup->stringify( ) ). + + ENDMETHOD. + +ENDCLASS. +``` + +## The Structure + +- **`main` is a pure dispatcher.** It stashes `client` in a protected + attribute — so the handler methods can use it without passing it around — + and routes each roundtrip to the method for its phase. `check_on_event( )` + without an argument is true for *any* event; the `CASE` in `on_event` + decides which one. +- **State stays public, everything else protected.** Public attributes are + the serialized, browser-visible model — bound data and nothing more. The + `client` reference and the methods are implementation. +- **One method per screen.** `view_display` for the page, `popup_edit_display` + for the dialog. When an app grows, this is the seam it grows along. + +## Where to Go From Here + +You have seen everything an abap2UI5 app is made of. Three places continue +from here: + +- the [Full Example](/get_started/full_example) — the same structure applied + to a realistic selection-screen app with a table and database access, +- the [Cookbook](/cookbook/overview) — every topic of this walkthrough as a + reference chapter, from [value helps](/cookbook/expert_more/value_help) to + [navigation between apps](/cookbook/event_navigation/navigation), +- the [sample catalogues](https://abap2ui5.github.io/samples/) — complete, + tested apps for nearly every pattern, each one class like here.