diff --git a/.gitignore b/.gitignore index 744d4ac..09f3769 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ node_modules/ +# no lockfile, as in cap2UI5: CI tests what an install gets today (.github/workflows/test.yml) +package-lock.json *.sqlite *.sqlite-* *.log diff --git a/ABAP2UI5_SAMPLES_PIN b/ABAP2UI5_SAMPLES_PIN index 20724f9..d3f02ce 100644 --- a/ABAP2UI5_SAMPLES_PIN +++ b/ABAP2UI5_SAMPLES_PIN @@ -1 +1 @@ -cdd5c8b09a4ecdc51943b827ff6711d6c3af13a5 +2e998ef1c90a1350e4ce1677bba59122fc8dac96 diff --git a/CHANGELOG.md b/CHANGELOG.md index 67222b1..cf96b29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,27 @@ versions [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- The pin moves to abap2UI5/samples `2e998ef` (2026-09-30). `Z2UI5_CL_SMP_APP_173` + is translated again from its reworked original - a metadata-driven table + and form with `template:with`, nested `template:repeat` and + `template:if/elseif/else`. `Z2UI5_CL_SMP_APP_176` gained a button that + re-renders only the nested view, through an `ASSIGN mt_layout[ … ] TO + FIELD-SYMBOL( )` abap2js refuses, so it is ported by hand from here on and + listed under `handwritten`. +- Five of the nine samples abap2UI5/samples added since, translated by + abap2js: `Z2UI5_CL_SMP_APP_535` (CustomData writeToDom), + `Z2UI5_CL_SMP_APP_536` (custom data on controls), `Z2UI5_CL_SMP_APP_537` + (expressions, formatters and literals in `t_arg`), and two helper apps - + `Z2UI5_CL_SMP_APP_533`, the wizard with page transitions + `Z2UI5_CL_SMP_APP_531` calls, and `Z2UI5_CL_SMP_APP_534`, the popup-as-app + `Z2UI5_CL_SMP_APP_532` calls. 76 of abap2UI5's 138 samples. The other four + wait for abap2js: 531 reads a constant of another class, 532 uses `NEW` + outside the navigation, 538 `VALUE #( OPTIONAL … )`, 539 `SPLIT`. +- `npm run generate` (and its `--check`, the CI gate) refuses a module in + `srv/apps/` that `scripts/samples.json` does not list - such a module is + packed, served and started by the tests, but never held to its original by + the differential test, which reads `samples.json` - and names a listed + sample the checkout at the pin does not have instead of failing on the read. - The differential test transpiles the originals against open-abap-core at the commit `@abap2ui5/node-runtime` records in its `package.json` (`abap2ui5.openAbapCore`) instead of HEAD, so the original runs on the diff --git a/README.md b/README.md index 9b0a010..ea24079 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ original's comments and texts, all of them, as they are. `client->check_app_prev_stack( )` is `client.check_app_prev_stack()`. > [!NOTE] -> **Status: 71 of the 129 samples.** 69 are generated by abap2js from +> **Status: 76 of the 138 samples.** 73 are generated by abap2js from > abap2UI5/samples at the commit in [`ABAP2UI5_SAMPLES_PIN`](ABAP2UI5_SAMPLES_PIN), -> two are ported by hand; the [differential test](#the-differential-test) holds +> three are ported by hand; the [differential test](#the-differential-test) holds > every one of them to its ABAP original. ## Add them to your project @@ -76,6 +76,7 @@ open e.g. . | CSS - Color Table Cells from the Backend | [`Z2UI5_CL_SMP_APP_305`](srv/apps/z2ui5_cl_smp_app_305.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_305.clas.abap) | | CSS - FlexBox Layouts with Custom Classes | [`Z2UI5_CL_SMP_APP_255`](srv/apps/z2ui5_cl_smp_app_255.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_255.clas.abap) | | CSS - Ship Your Own CSS with the View | [`Z2UI5_CL_SMP_APP_050`](srv/apps/z2ui5_cl_smp_app_050.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_050.clas.abap) | +| CSS - Style by Data with CustomData writeToDom | [`Z2UI5_CL_SMP_APP_535`](srv/apps/z2ui5_cl_smp_app_535.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_535.clas.abap) | | Control Behaviour - Aggregation Item by Index | [`Z2UI5_CL_SMP_APP_514`](srv/apps/z2ui5_cl_smp_app_514.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_514.clas.abap) | | Control Behaviour - Expand a Panel by ID (setExpanded) | [`Z2UI5_CL_SMP_APP_448`](srv/apps/z2ui5_cl_smp_app_448.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_448.clas.abap) | | Control Behaviour - Inline CSS on a Control (css) | [`Z2UI5_CL_SMP_APP_513`](srv/apps/z2ui5_cl_smp_app_513.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_513.clas.abap) | @@ -87,6 +88,8 @@ open e.g. . | Device - Device Model: Phone, Tablet, Desktop | [`Z2UI5_CL_SMP_APP_445`](srv/apps/z2ui5_cl_smp_app_445.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_445.clas.abap) | | Device - Frontend Info: UI5 Version, Theme, OS, Browser | [`Z2UI5_CL_SMP_APP_122`](srv/apps/z2ui5_cl_smp_app_122.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_122.clas.abap) | | Device - Geolocation from the Browser | [`Z2UI5_CL_SMP_APP_120`](srv/apps/z2ui5_cl_smp_app_120.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_120.clas.abap) | +| Event - Custom Data Attached to Controls | [`Z2UI5_CL_SMP_APP_536`](srv/apps/z2ui5_cl_smp_app_536.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_536.clas.abap) | +| Event - Expressions, Formatters and Literals in t_arg | [`Z2UI5_CL_SMP_APP_537`](srv/apps/z2ui5_cl_smp_app_537.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_537.clas.abap) | | Event - Extra Arguments with t_arg | [`Z2UI5_CL_SMP_APP_167`](srv/apps/z2ui5_cl_smp_app_167.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_167.clas.abap) | | Event - Keep the Last Keystroke with check_queue_last | [`Z2UI5_CL_SMP_APP_511`](srv/apps/z2ui5_cl_smp_app_511.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_511.clas.abap) | | Event - Link with preventDefault | [`Z2UI5_CL_SMP_APP_472`](srv/apps/z2ui5_cl_smp_app_472.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_472.clas.abap) | @@ -110,6 +113,7 @@ open e.g. . | Navigation - Data Loss Protection on Leaving | [`Z2UI5_CL_SMP_APP_279`](srv/apps/z2ui5_cl_smp_app_279.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_279.clas.abap) | | Navigation - Detail Page | [`Z2UI5_CL_SMP_APP_469`](srv/apps/z2ui5_cl_smp_app_469.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_469.clas.abap) | | Navigation - Return Data and Events to the Caller | [`Z2UI5_CL_SMP_APP_488`](srv/apps/z2ui5_cl_smp_app_488.js) - by hand | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_488.clas.abap) | +| Navigation - Wizard Steps with Page Transitions (the app Z2UI5_CL_SMP_APP_531 calls) | [`Z2UI5_CL_SMP_APP_533`](srv/apps/z2ui5_cl_smp_app_533.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_533.clas.abap) | | Nested View - Basic Example (nest_view_display) | [`Z2UI5_CL_SMP_APP_065`](srv/apps/z2ui5_cl_smp_app_065.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_065.clas.abap) | | Nested View - Destroy and Target a Slot | [`Z2UI5_CL_SMP_APP_510`](srv/apps/z2ui5_cl_smp_app_510.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_510.clas.abap) | | Popover - Basic Example with Placement | [`Z2UI5_CL_SMP_APP_026`](srv/apps/z2ui5_cl_smp_app_026.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_026.clas.abap) | @@ -119,9 +123,10 @@ open e.g. . | Popup - Dialog inside a Dialog | [`Z2UI5_CL_SMP_APP_161`](srv/apps/z2ui5_cl_smp_app_161.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_161.clas.abap) | | Popup - Element Binding to the Selected Row | [`Z2UI5_CL_SMP_APP_470`](srv/apps/z2ui5_cl_smp_app_470.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_470.clas.abap) | | Popup - Navigate between Dialogs (NavContainer) | [`Z2UI5_CL_SMP_APP_170`](srv/apps/z2ui5_cl_smp_app_170.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_170.clas.abap) | +| Popup - Popup-as-App: a Dialog and no View (the app Z2UI5_CL_SMP_APP_532 calls) | [`Z2UI5_CL_SMP_APP_534`](srv/apps/z2ui5_cl_smp_app_534.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_534.clas.abap) | | Table - Editable Cells, Add and Delete Rows | [`Z2UI5_CL_SMP_APP_011`](srv/apps/z2ui5_cl_smp_app_011.js) - by hand | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_011.clas.abap) | -| Templating - Build Columns Dynamically (template:repeat) | [`Z2UI5_CL_SMP_APP_173`](srv/apps/z2ui5_cl_smp_app_173.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_173.clas.abap) | -| Templating - Dynamic Content in a Nested View | [`Z2UI5_CL_SMP_APP_176`](srv/apps/z2ui5_cl_smp_app_176.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_176.clas.abap) | +| Templating - Metadata-Driven Table and Form | [`Z2UI5_CL_SMP_APP_173`](srv/apps/z2ui5_cl_smp_app_173.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_173.clas.abap) | +| Templating - Dynamic Content in a Nested View | [`Z2UI5_CL_SMP_APP_176`](srv/apps/z2ui5_cl_smp_app_176.js) - by hand | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_176.clas.abap) | | Tree - Inside a Dialog | [`Z2UI5_CL_SMP_APP_462`](srv/apps/z2ui5_cl_smp_app_462.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_462.clas.abap) | | Tree - Nested ABAP Table in a sap.m.Tree | [`Z2UI5_CL_SMP_APP_460`](srv/apps/z2ui5_cl_smp_app_460.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_460.clas.abap) | @@ -140,9 +145,10 @@ is made: - **`handwritten`** - a class abap2js refuses, ported by hand the way abap2js would write it, with the construct it refuses as the reason: `Z2UI5_CL_SMP_APP_011` (`LOOP AT t_tab REFERENCE INTO` writes through the - row, and `DELETE t_tab WHERE`) and `Z2UI5_CL_SMP_APP_488` - (`ASSIGN … ->* TO FIELD-SYMBOL( )`). A sample moves to `generated` when - abap2js learns the construct. + row, and `DELETE t_tab WHERE`), `Z2UI5_CL_SMP_APP_176` + (`ASSIGN mt_layout[ … ] TO FIELD-SYMBOL( )` writes through the row) and + `Z2UI5_CL_SMP_APP_488` (`ASSIGN … ->* TO FIELD-SYMBOL( )`). A sample moves + to `generated` when abap2js learns the construct. The checkout is `--samples `, else `$ABAP2UI5_SAMPLES`, else `../abap2UI5-samples`, and it has to be at the pin - `@origin` in a module @@ -171,8 +177,8 @@ against the ABAP the runtime ships - renames it `ZABAP_SMP_APP_nnn` and serves it beside the translation in one cap2UI5 server. Both are started, every event the original's first view wires is fired on a fresh start of each, and what comes back is compared: the XML of every view slot, the model the original -sends, every other action and which app has the screen - today 71 samples in -239 roundtrips, without a difference. One is known and left on purpose: +sends, every other action and which app has the screen - today 76 samples in +260 roundtrips, without a difference. One is known and left on purpose: `Z2UI5_CL_SMP_APP_122` prints a number with `CONV string( )`, which keeps ABAP's trailing sign position (`"0 "`) and JavaScript's `String( )` does not. diff --git a/scripts/generate.mjs b/scripts/generate.mjs index 596fad9..a6222c4 100644 --- a/scripts/generate.mjs +++ b/scripts/generate.mjs @@ -50,11 +50,26 @@ if (overlap.length) { console.error(`generate: ${overlap.join(", ")} listed as generated AND handwritten in scripts/samples.json`); process.exit(1); } +// A module in srv/apps that samples.json does not list is packed, served and +// started by the tests like any other - but never held to an original by the +// differential test, which reads samples.json. So it is listed, or it goes. +const orphans = fs.readdirSync(path.join(ROOT, "srv", "apps")) + .filter((f) => /^z2ui5_cl_smp_app_\d+\.js$/.test(f)).map((f) => f.replace(/\.js$/, "")) + .filter((n) => !generated.includes(n) && !(n in handwritten)); +if (orphans.length) { + console.error(`generate: srv/apps/${orphans.join(".js, srv/apps/")}.js not listed in scripts/samples.json - list it, or delete it`); + process.exit(1); +} let failed = 0; let changed = 0; for (const name of generated) { const file = path.join(src, `${name}.clas.abap`); + if (!fs.existsSync(file)) { + console.error(`missing: src/${name}.clas.abap is not in abap2UI5/samples@${pin.slice(0, 7)} - is the name right?`); + failed++; + continue; + } let code; try { ({ code } = abap2js(fs.readFileSync(file, "utf8"), { diff --git a/scripts/originals.mjs b/scripts/originals.mjs index a52d895..0c3cca1 100644 --- a/scripts/originals.mjs +++ b/scripts/originals.mjs @@ -30,6 +30,11 @@ const pin = fs.readFileSync(path.join(ROOT, "ABAP2UI5_SAMPLES_PIN"), "utf8").tri const { generated, handwritten } = JSON.parse(fs.readFileSync(path.join(ROOT, "scripts", "samples.json"), "utf8")); const names = [...generated, ...Object.keys(handwritten)]; +if (!fs.existsSync(path.join(checkout, "src"))) { + console.error(`originals: no abap2UI5/samples checkout at ${checkout} - clone it and check out ${pin}:\n` + + ` git clone https://github.com/abap2UI5/samples ${checkout} && git -C ${checkout} checkout ${pin}`); + process.exit(1); +} const head = execFileSync("git", ["-C", checkout, "rev-parse", "HEAD"], { encoding: "utf8" }).trim(); if (head !== pin) throw new Error(`${checkout} is at ${head}, ABAP2UI5_SAMPLES_PIN says ${pin}`); diff --git a/scripts/samples.json b/scripts/samples.json index 573bbaf..8c1e72a 100644 --- a/scripts/samples.json +++ b/scripts/samples.json @@ -19,7 +19,6 @@ "z2ui5_cl_smp_app_167", "z2ui5_cl_smp_app_170", "z2ui5_cl_smp_app_173", - "z2ui5_cl_smp_app_176", "z2ui5_cl_smp_app_186", "z2ui5_cl_smp_app_189", "z2ui5_cl_smp_app_202", @@ -69,10 +68,16 @@ "z2ui5_cl_smp_app_515", "z2ui5_cl_smp_app_516", "z2ui5_cl_smp_app_518", - "z2ui5_cl_smp_app_519" + "z2ui5_cl_smp_app_519", + "z2ui5_cl_smp_app_533", + "z2ui5_cl_smp_app_534", + "z2ui5_cl_smp_app_535", + "z2ui5_cl_smp_app_536", + "z2ui5_cl_smp_app_537" ], "handwritten": { "z2ui5_cl_smp_app_011": "LOOP AT t_tab REFERENCE INTO writes through the row, and DELETE t_tab WHERE (abap2js: neither supported yet)", + "z2ui5_cl_smp_app_176": "ASSIGN mt_layout[ fname = ... ] TO FIELD-SYMBOL( ) writes through the row (abap2js: ASSIGN not supported yet)", "z2ui5_cl_smp_app_488": "ASSIGN ls_get-r_event_data->* TO FIELD-SYMBOL( ) (abap2js: not supported yet)" } } diff --git a/srv/apps/z2ui5_cl_smp_app_173.js b/srv/apps/z2ui5_cl_smp_app_173.js index 616d06b..0fd7f6e 100644 --- a/srv/apps/z2ui5_cl_smp_app_173.js +++ b/srv/apps/z2ui5_cl_smp_app_173.js @@ -1,7 +1,15 @@ -// @keywords template repeat runtime generated columns if then else -// @summary Builds the columns of a table at runtime with template:repeat, including the if/then/else the templating language brings. +// @keywords xml templating xmlpreprocessor template:repeat template:if template:elseif template:with meta model metamodel metadata driven dynamic columns form nested repeat startindex length +// @summary XML templating driven by a meta model: table columns built with template:repeat, a form generated from a field catalogue with nested template:repeat, template:with and template:if/elseif/else, and a template:if that re-renders on a switch. // @docs https://abap2ui5.github.io/docs/cookbook/view/xml_templating // @origin abap2UI5/samples src/z2ui5_cl_smp_app_173.clas.abap +// +// The view is expanded by the UI5 XMLPreprocessor before its controls exist. +// Its input is a meta model: data ABOUT the view - which columns a table has, +// which fields a form has and of which type - instead of the data it shows. +// A UI5 app would hand the preprocessor an OData meta model; here the meta +// model is plain ABAP data, bound like any attribute and read under the +// template> model name. The rows the controls then display stay ordinary +// runtime bindings against the default model. import { defineApp, t, z2ui5_cl_ui5_view_builder } from "@cap2ui5/cds-plugin"; const ty_s_data = { @@ -16,19 +24,47 @@ const ty_s_layout = { visible: "", }; +const ty_s_field = { + name: "", + label: "", + type: "", +}; + +const ty_s_group = { + title: "", + t_field: t.table(ty_s_field), +}; + +const ty_s_meta = { + entity: "", + t_group: t.table(ty_s_group), +}; + +const ty_s_detail = { + name: "", + date: "", + city: "", + age: 0, + active: false, +}; + defineApp("Z2UI5_CL_SMP_APP_173", class { - mv_flag = false; + mv_flag = false; mt_layout = t.table(ty_s_layout); mt_data = t.table(ty_s_data); + ms_meta = ty_s_meta; + ms_detail = ty_s_detail; view_display() { - // the template model is the view model, so what the repeat and the if - // read are bound attributes - their paths are composed from the bind - // call, never written by hand + // the template model is the view model, so what the repeat, the with and + // the if read are bound attributes - their paths are composed from the + // bind call, never written by hand const layout_path = `{template>${this.client._bind({ val: "mt_layout", path: true })}}`; const flag_path = `{template>${this.client._bind({ val: "mv_flag", path: true })}}`; + const meta_path = `template>${this.client._bind({ val: "ms_meta", path: true })}`; + const detail_path = `{${this.client._bind({ val: "ms_detail", path: true })}}`; let view = z2ui5_cl_ui5_view_builder.factory() .ele({ n: "View", ns: "mvc" }) @@ -37,23 +73,30 @@ defineApp("Z2UI5_CL_SMP_APP_173", class { .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:form", v: "sap.ui.layout.form" }) .a({ n: "xmlns:template", v: "http://schemas.sap.com/sapui5/extension/sap.ui.core.template/1" }); view = view.ele("Shell") .ele("Page") - .a({ n: "title", v: "abap2UI5 - Templating - Build Columns Dynamically (template:repeat)" }) + .a({ n: "title", v: "abap2UI5 - Templating - Metadata-Driven Table and Form" }) .a({ n: "showNavButton", b: this.client.check_app_prev_stack() }) .a({ n: "navButtonPress", v: this.client._event_nav_app_leave() }) .a({ n: "class", v: "sapUiContentPadding" }) .a({ n: "id", v: "page_main" }); view.tag("MessageStrip") - .a({ n: "text", v: "This sample builds table columns and cells dynamically from a layout table " + - "using template repeat, plus a template if/then/else that re-renders on a switch." }) + .a({ n: "text", v: "XML templating expands the view from a meta model before any control exists: " + + "the table columns come from a layout table (template:repeat), the form from a field catalogue " + + "(template:with, nested template:repeat, template:if/elseif/else), and the icon below re-renders on the switch." }) .a({ n: "type", v: "Information" }) .a({ n: "showIcon", b: true }) .a({ n: "class", v: "sapUiSmallMargin" }); + // 1 - columns and cells from the layout table + view.tag("Title") + .a({ n: "text", v: "Table - columns from a layout table (template:repeat)" }) + .a({ n: "class", v: "sapUiSmallMarginTop" }); + view.ele("Table") .a({ n: "items", v: this.client._bind("mt_data") }) .ele("columns") @@ -77,8 +120,73 @@ defineApp("Z2UI5_CL_SMP_APP_173", class { .ele("ObjectIdentifier") .a({ n: "text", v: "{= '{' + ${L1>FNAME} + '}' }" }); - view.tag("Label") - .a({ n: "text", v: "IF Template (with re-rendering)" }); + // 2 - a form from the field catalogue of the meta model. template:with + // gives the meta model a short name, the outer repeat builds one title + // per group, the inner repeat one label and one control per field, and + // the if/elseif/else picks the control from the field's type + view.tag("Title") + .a({ n: "text", v: "Form - fields from a meta model (template:with, nested repeat, if/elseif/else)" }) + .a({ n: "class", v: "sapUiMediumMarginTop" }); + + const box = view.ele({ n: "with", ns: "template" }) + .a({ n: "path", v: meta_path }) + .a({ n: "var", v: "meta" }) + .ele("VBox") + .a({ n: "binding", v: detail_path }); + + // startIndex and length cut the list - the header shows the first two + // fields of the first group only + box.ele("ObjectHeader") + .a({ n: "title", v: "{meta>ENTITY}" }) + .ele("attributes") + .ele({ n: "repeat", ns: "template" }) + .a({ n: "list", v: "{path: 'meta>T_GROUP/0/T_FIELD', startIndex: 0, length: 2}" }) + .a({ n: "var", v: "head" }) + .tag("ObjectAttribute") + .a({ n: "title", v: "{head>LABEL}" }) + .a({ n: "text", v: "{= '{' + ${head>NAME} + '}' }" }); + + const field = box.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "editable", b: true }) + .a({ n: "layout", v: "ResponsiveGridLayout" }) + .ele({ n: "content", ns: "form" }) + .ele({ n: "repeat", ns: "template" }) + .a({ n: "list", v: "{meta>T_GROUP}" }) + .a({ n: "var", v: "group" }) + .tag({ n: "Title", ns: "core" }) + .a({ n: "text", v: "{group>TITLE}" }) + .ele({ n: "repeat", ns: "template" }) + .a({ n: "list", v: "{group>T_FIELD}" }) + .a({ n: "var", v: "field" }); + + field.tag("Label") + .a({ n: "text", v: "{field>LABEL}" }); + + field.ele({ n: "if", ns: "template" }) + .a({ n: "test", v: "{= ${field>TYPE} === 'BOOLEAN' }" }) + .ele({ n: "then", ns: "template" }) + .tag("Switch") + .a({ n: "state", v: "{= '{' + ${field>NAME} + '}' }" }) + .end() + .ele({ n: "elseif", ns: "template" }) + .a({ n: "test", v: "{= ${field>TYPE} === 'DATE' }" }) + .tag("DatePicker") + .a({ n: "value", v: "{= '{' + ${field>NAME} + '}' }" }) + .a({ n: "valueFormat", v: "yyyy-MM-dd" }) + .end() + .ele({ n: "elseif", ns: "template" }) + .a({ n: "test", v: "{= ${field>TYPE} === 'NUMBER' }" }) + .tag("StepInput") + .a({ n: "value", v: "{= '{' + ${field>NAME} + '}' }" }) + .end() + .ele({ n: "else", ns: "template" }) + .tag("Input") + .a({ n: "value", v: "{= '{' + ${field>NAME} + '}' }" }); + + // 3 - an if on a bound flag, re-rendered by the switch + view.tag("Title") + .a({ n: "text", v: "IF Template (with re-rendering)" }) + .a({ n: "class", v: "sapUiMediumMarginTop" }); view.tag("Switch") .a({ n: "state", v: this.client._bind("mv_flag") }) .a({ n: "change", v: this.client._event("CHANGE_FLAG") }); @@ -100,21 +208,44 @@ defineApp("Z2UI5_CL_SMP_APP_173", class { } - main(client) { + model_init() { - this.client = client; + this.mt_data = [ { name: "Theo", date: "01.01.2000", age: "5" }, + { name: "Lore", date: "01.01.2000", age: "1" } ]; - if (client.check_on_init()) { + this.mt_layout = [ { fname: "NAME", merge: "false", visible: "true" }, + { fname: "DATE", merge: "false", visible: "true" }, + { fname: "AGE", merge: "false", visible: "false" } ]; - this.mt_data = [ { name: "Theo", date: "01.01.2000", age: "5" }, - { name: "Lore", date: "01.01.2000", age: "1" } ]; + // the meta model - which fields the form has, in which group, of which + // type. Reorder a field, move it to the other group or change its type + // and the generated form follows, no view code changes + this.ms_meta = { + entity: "Person", + t_group: [ + { title: "General", + t_field: [ { name: "NAME", label: "Name", type: "STRING" }, + { name: "DATE", label: "Birth Date", type: "DATE" }, + { name: "CITY", label: "City", type: "STRING" } ] }, + { title: "Details", + t_field: [ { name: "AGE", label: "Age", type: "NUMBER" }, + { name: "ACTIVE", label: "Active", type: "BOOLEAN" } ] } ] }; - this.mt_layout = [ { fname: "NAME", merge: "false", visible: "true" }, - { fname: "DATE", merge: "false", visible: "true" }, - { fname: "AGE", merge: "false", visible: "false" } ]; + this.ms_detail = { name: "Lore", + date: "2000-01-01", + city: "Walldorf", + age: 26, + active: true }; - this.view_display(); + } + + main(client) { + this.client = client; + + if (client.check_on_init()) { + this.model_init(); + this.view_display(); } else if (client.check_on_navigated()) { this.view_display(); } else if (client.check_on_event("CHANGE_FLAG")) { diff --git a/srv/apps/z2ui5_cl_smp_app_176.js b/srv/apps/z2ui5_cl_smp_app_176.js index f2bb00c..fb3f005 100644 --- a/srv/apps/z2ui5_cl_smp_app_176.js +++ b/srv/apps/z2ui5_cl_smp_app_176.js @@ -1,5 +1,5 @@ -// @keywords template repeat runtime generated nested nest_view_display -// @summary XML templating inside a nested view: the generated content is built where the sub view is rendered. +// @keywords xml templating xmlpreprocessor template:repeat meta model metamodel metadata driven dynamic columns nested view nest_view_display re-render refresh +// @summary XML templating inside a nested view: the columns come from a layout table via template:repeat, and a change to that table re-renders only the nested view with nest_view_display while the main view stays on screen. // @docs https://abap2ui5.github.io/docs/cookbook/view/nested_views https://abap2ui5.github.io/docs/cookbook/view/xml_templating // @origin abap2UI5/samples src/z2ui5_cl_smp_app_176.clas.abap import { defineApp, t, z2ui5_cl_ui5_view_builder } from "@cap2ui5/cds-plugin"; @@ -43,6 +43,16 @@ defineApp("Z2UI5_CL_SMP_APP_176", class { this.view_display(); this.nest_view_display(); + } else if (client.check_on_event("TOGGLE_AGE")) { + + // templating ran once, when the nested view was built - a changed + // layout table only shows once the nested view is built again. The + // main view is not sent, it stays on screen as it is + // (a read of mt_layout is a copy: the AGE row is written back whole) + this.mt_layout = this.mt_layout.map((age) => (age.fname === "AGE" + ? { ...age, visible: age.visible === "true" ? "false" : "true" } : age)); + this.nest_view_display(); + } } @@ -60,17 +70,28 @@ defineApp("Z2UI5_CL_SMP_APP_176", class { .ele("Page") .a({ n: "title", v: "abap2UI5 - Templating - Dynamic Content in a Nested View" }) .a({ n: "showNavButton", b: this.client.check_app_prev_stack() }) - .a({ n: "navButtonPress", v: this.client._event_nav_app_leave() }) - .a({ n: "id", v: "test" }); + .a({ n: "navButtonPress", v: this.client._event_nav_app_leave() }); page.tag("MessageStrip") .a({ n: "text", v: "This sample renders a main view and then embeds a second view into it as " + "nested content via nest_view_display; the nested table builds its columns and cells " + - "at runtime with template:repeat over a layout table." }) + "at runtime with template:repeat over a layout table. The button changes that table and " + + "re-renders only the nested view." }) .a({ n: "type", v: "Information" }) .a({ n: "showIcon", b: true }) .a({ n: "class", v: "sapUiSmallMargin" }); + page.tag("Button") + .a({ n: "text", v: "Show / Hide Age Column" }) + .a({ n: "icon", v: "sap-icon://refresh" }) + .a({ n: "press", v: this.client._event("TOGGLE_AGE") }) + .a({ n: "class", v: "sapUiSmallMarginBegin" }); + + // the nested view goes into this box - a container of its own, so a + // re-render replaces the nested view and nothing else on the page + page.tag("VBox") + .a({ n: "id", v: "box_nest" }); + this.client.view_display(lo_view.stringify()); } @@ -85,38 +106,40 @@ defineApp("Z2UI5_CL_SMP_APP_176", class { const lo_view_nested = 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:template", v: "http://schemas.sap.com/sapui5/extension/sap.ui.core.template/1" }); - lo_view_nested.ele("Shell") - .ele("Page") - .a({ n: "title", v: "Nested View" }) - .ele("Table") - .a({ n: "items", v: this.client._bind("mt_data") }) - .ele("columns") - .ele({ n: "repeat", ns: "template" }) - .a({ n: "list", v: layout_path }) - .a({ n: "var", v: "LO" }) - .ele("Column") - .a({ n: "mergeDuplicates", v: "{LO>MERGE}" }) - .a({ n: "visible", v: "{LO>VISIBLE}" }) - .tag("Text") - .a({ n: "text", v: "{LO>TITLE}" }) - .end() + lo_view_nested.ele("Panel") + .a({ n: "headerText", v: "Nested View" }) + .a({ n: "class", v: "sapUiSmallMarginTop" }) + .ele("Table") + .a({ n: "items", v: this.client._bind("mt_data") }) + .ele("columns") + .ele({ n: "repeat", ns: "template" }) + .a({ n: "list", v: layout_path }) + .a({ n: "var", v: "LO" }) + .ele("Column") + .a({ n: "mergeDuplicates", v: "{LO>MERGE}" }) + .a({ n: "visible", v: "{LO>VISIBLE}" }) + .tag("Text") + .a({ n: "text", v: "{LO>TITLE}" }) .end() .end() - .ele("items") - .ele("ColumnListItem") - .ele("cells") - .ele({ n: "repeat", ns: "template" }) - .a({ n: "list", v: layout_path }) - .a({ n: "var", v: "LO2" }) - .ele("ObjectIdentifier") - .a({ n: "text", v: "{= '{' + ${LO2>FNAME} + '}' }" }); - - this.client.nest_view_display({ val: lo_view_nested.stringify(), id: "test", method_insert: "addContent", method_destroy: "removeAllContent" }); + .end() + .ele("items") + .ele("ColumnListItem") + .ele("cells") + .ele({ n: "repeat", ns: "template" }) + .a({ n: "list", v: layout_path }) + .a({ n: "var", v: "LO2" }) + .ele("ObjectIdentifier") + .a({ n: "text", v: "{= '{' + ${LO2>FNAME} + '}' }" }); + + this.client.nest_view_display({ val: lo_view_nested.stringify(), + id: "box_nest", + method_insert: "addItem", + method_destroy: "removeAllItems" }); } }); diff --git a/srv/apps/z2ui5_cl_smp_app_533.js b/srv/apps/z2ui5_cl_smp_app_533.js new file mode 100644 index 0000000..bdeb4c7 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_533.js @@ -0,0 +1,100 @@ +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_533.clas.abap +// An app with screens of its own, called by z2ui5_cl_smp_app_531: Next and +// Previous rebuild ITS view, no other app is involved - so nothing tells the +// framework which way the screen moves. Next is forward by default; Previous +// says it with view_display( transition_back = abap_true ) and plays the +// step being left out in reverse. Done leaves the app with nav_app_leave( ), +// reversed too. This app is a hidden helper (never listed on its own in the +// overview). +import { defineApp, z2ui5_cl_ui5_view_builder } from "@cap2ui5/cds-plugin"; + +const steps = 3; + +defineApp("Z2UI5_CL_SMP_APP_533", class { + + step = 1; + + main(client) { + + this.client = client; + + if (client.check_on_navigated()) { + this.view_display(); + + } else if (client.check_on_event("NEXT")) { + + this.step = this.step + 1; + this.view_display(); + + } else if (client.check_on_event("PREVIOUS")) { + + this.step = this.step - 1; + this.view_display(true); + + } else if (client.check_on_event("DONE")) { + client.nav_app_leave(); + } + + } + + view_display(back) { + + const color = (this.step === 1 ? "#0a6ed1" + : this.step === 2 ? "#e9730c" + : "#107e3e"); + + const 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" }); + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", t: `abap2UI5 - Wizard - Step ${this.step} of ${steps}` }) + .a({ n: "showNavButton", b: this.client.check_app_prev_stack() }) + .a({ n: "navButtonPress", v: this.client._event_nav_app_leave() }); + + page.tag("MessageStrip") + .a({ n: "id", v: "step" }) + .a({ n: "text", t: `Step ${this.step} of ${steps} - the same app, a new view each time. Next moves forward, ` + + `Previous says it goes back: view_display( transition_back = abap_true ).` }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + const content = page.ele("VBox") + .a({ n: "class", v: "sapUiSmallMargin" }); + + content.tag({ n: "Icon", ns: "core" }) + .a({ n: "src", v: "sap-icon://step" }) + .a({ n: "size", v: "6rem" }) + .a({ n: "color", t: color }) + .a({ n: "class", v: "sapUiMediumMarginTopBottom" }); + + content.tag("Button") + .a({ n: "id", v: "previous" }) + .a({ n: "text", v: "Previous - view_display( transition_back = abap_true )" }) + .a({ n: "icon", v: "sap-icon://navigation-left-arrow" }) + .a({ n: "enabled", b: (this.step > 1) }) + .a({ n: "class", v: "sapUiTinyMarginBottom" }) + .a({ n: "press", v: this.client._event("PREVIOUS") }); + content.tag("Button") + .a({ n: "id", v: "next" }) + .a({ n: "text", v: "Next - view_display( transition = slide )" }) + .a({ n: "icon", v: "sap-icon://navigation-right-arrow" }) + .a({ n: "enabled", b: (this.step < steps) }) + .a({ n: "type", v: "Emphasized" }) + .a({ n: "class", v: "sapUiTinyMarginBottom" }) + .a({ n: "press", v: this.client._event("NEXT") }); + content.tag("Button") + .a({ n: "id", v: "done" }) + .a({ n: "text", v: "Done - nav_app_leave( ): back to the caller" }) + .a({ n: "icon", v: "sap-icon://accept" }) + .a({ n: "press", v: this.client._event("DONE") }); + + this.client.view_display({ val: view.stringify(), transition: this.client.cs_transition.slide, transition_back: back }); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_534.js b/srv/apps/z2ui5_cl_smp_app_534.js new file mode 100644 index 0000000..e95e8b4 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_534.js @@ -0,0 +1,41 @@ +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_534.clas.abap +// A popup-as-app, called by the page z2ui5_cl_smp_app_532: nav_app_call( ) +// opens an app that shows a dialog and no view of its own, so its caller's +// page stays on screen. Closing hands back with nav_app_leave( ), the caller +// re-displays - and the page does NOT move: the display carries the app +// instance, and the frontend sees that the page on screen belongs to it. +// This app is a hidden helper (never listed on its own in the overview). +import { defineApp, z2ui5_cl_ui5_view_builder } from "@cap2ui5/cds-plugin"; + +defineApp("Z2UI5_CL_SMP_APP_534", class { + + main(client) { + + if (client.check_on_navigated()) { + + const popup = z2ui5_cl_ui5_view_builder.factory(); + const dialog = popup.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: "A popup-as-app" }); + dialog.tag("Text") + .a({ n: "text", v: "This app shows a dialog and nothing else. Close it: the page behind it comes back " + + "without a transition - it never left." }) + .a({ n: "class", v: "sapUiSmallMargin" }); + dialog.ele("buttons") + .tag("Button") + .a({ n: "id", v: "close" }) + .a({ n: "text", v: "Close" }) + .a({ n: "press", v: client._event("CLOSE") }); + client.popup_display(popup.stringify()); + + } else if (client.check_on_event("CLOSE")) { + + client.popup_destroy(); + client.nav_app_leave(); + + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_535.js b/srv/apps/z2ui5_cl_smp_app_535.js new file mode 100644 index 0000000..ee0733f --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_535.js @@ -0,0 +1,198 @@ +// @keywords customdata writetodom data attribute selector marker test anchor +// @summary Writes bound values into the HTML DOM as data-* attributes with CustomData writeToDom, so a stylesheet colours controls by their data and tests find stable anchors. +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_535.clas.abap +import { defineApp, t, z2ui5_cl_ui5_view_builder } from "@cap2ui5/cds-plugin"; + +const ty_s_order = { + id: "", + product: "", + status: "", +}; + +defineApp("Z2UI5_CL_SMP_APP_535", class { + + status = ""; + t_orders = t.table(ty_s_order); + + main(client) { + + this.client = client; + + if (client.check_on_init()) { + + this.status = "new"; + this.t_orders = [ + { id: "4711", product: "Notebook", status: "new" }, + { id: "4712", product: "Monitor", status: "shipped" }, + { id: "4713", product: "Keyboard", status: "delayed" }, + { id: "4714", product: "Mouse", status: "shipped" }, + { id: "4715", product: "Headset", status: "delayed" } ]; + this.view_display(); + + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event("NEXT")) { + this.status_next(); + } + + } + + status_next() { + + this.status = (this.status === "new" ? "shipped" + : this.status === "shipped" ? "delayed" + : "new"); + + } + + view_display() { + + const 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" }); + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - CSS - Style by Data with CustomData writeToDom" }) + .a({ n: "showNavButton", b: this.client.check_app_prev_stack() }) + .a({ n: "navButtonPress", v: this.client._event_nav_app_leave() }); + + page.tag("MessageStrip") + .a({ n: "text", v: "A core:CustomData with writeToDom=\"true\" writes its key and value as a data-* attribute on the " + + "root element of its control. A stylesheet selects on that attribute to style controls by their data, " + + "and automated tests use it as a stable anchor that does not depend on generated IDs." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + // raw markup travels in the content attribute of a core:HTML leaf - the + // builder re-escapes it on stringify, so the literal markup is written here. + // Every rule selects on a data-* attribute, which exists only because the + // CustomData elements below carry writeToDom="true". The first rule is the + // one of the UI5 documentation, word for word + page.tag({ n: "HTML", ns: "core" }) + .a({ n: "content", v: "" }); + + // writeToDom needs the expanded notation: the app:key="value" shortcut + // creates a CustomData without the flag, so nothing reaches the DOM. The + // key must be a valid HTML ID - keep it lower case, browsers may lower it + page.ele("Panel") + .a({ n: "headerText", v: "A static value - the example of the documentation" }) + .a({ n: "class", v: "sapUiResponsiveMargin" }) + .a({ n: "width", v: "auto" }) + .ele("Button") + .a({ n: "text", v: "Renders as