From b70a33db8794e816b7eb52b82060d10ff3ebe60f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 18:22:49 +0000 Subject: [PATCH] Translate 69 abap2UI5 samples with cap2ui5 abap2js, texts 1:1 The samples are no longer ported by hand: scripts/generate.mjs reads each class from abap2UI5/samples at the commit in ABAP2UI5_SAMPLES_PIN and writes what cap2ui5's abap2js makes of it into srv/apps/.js - the same class name, the original's lines, comments and texts, all of them, the page title "abap2UI5 - ..." included. scripts/samples.json lists the 69 generated samples and the two abap2js still refuses, ported by hand the way it writes the rest, comments and texts 1:1 as well: 011 (LOOP AT ... REFERENCE INTO writes through the row, DELETE ... WHERE) and 488 (ASSIGN ... ->* TO FIELD-SYMBOL( )). Generated files are never edited by hand; `npm run check:generated` fails when one is not what the translation writes. test/differential.mjs holds every sample to its ABAP original: scripts/originals.mjs transpiles the originals with the transpiler the runtime was built with, renamed ZABAP_SMP_APP_nnn, and both are served in one cap2UI5 server; every event the original's first view wires is fired on a fresh start of each and the views, the model the original sends, the other actions and the target app are compared. 71 samples, 239 roundtrips, no difference; one known on purpose (122: CONV string( ) of a number keeps ABAP's sign position). CI runs both against the pinned samples checkout and packs cap2ui5 from the cap2UI5 branch that adds abap2js until it is on main. The samples test now expects the original's page title, and 186 showing no view on its first roundtrip, as its original does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012kUq3QqNUmt3WugBL5rLc7 --- .github/workflows/test.yml | 32 +++- .gitignore | 4 + ABAP2UI5_SAMPLES_PIN | 1 + README.md | 200 ++++++++++++++++++++----- package.json | 7 +- scripts/generate.mjs | 79 ++++++++++ scripts/originals.mjs | 88 +++++++++++ scripts/samples.json | 78 ++++++++++ srv/apps/z2ui5_cl_smp_app_011.js | 2 +- srv/apps/z2ui5_cl_smp_app_026.js | 138 ++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_027.js | 113 ++++++++++++++ srv/apps/z2ui5_cl_smp_app_050.js | 105 +++++++++++++ srv/apps/z2ui5_cl_smp_app_065.js | 115 +++++++++++++++ srv/apps/z2ui5_cl_smp_app_067.js | 154 ++++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_088.js | 82 +++++++++++ srv/apps/z2ui5_cl_smp_app_109.js | 147 +++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_120.js | 106 ++++++++++++++ srv/apps/z2ui5_cl_smp_app_122.js | 231 +++++++++++++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_125.js | 6 +- srv/apps/z2ui5_cl_smp_app_133.js | 94 ++++++++++++ srv/apps/z2ui5_cl_smp_app_143.js | 152 +++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_161.js | 5 +- srv/apps/z2ui5_cl_smp_app_163.js | 104 +++++++++++++ srv/apps/z2ui5_cl_smp_app_166.js | 97 ++++++++++++ srv/apps/z2ui5_cl_smp_app_167.js | 5 +- srv/apps/z2ui5_cl_smp_app_170.js | 168 +++++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_173.js | 125 ++++++++++++++++ srv/apps/z2ui5_cl_smp_app_176.js | 122 ++++++++++++++++ srv/apps/z2ui5_cl_smp_app_186.js | 105 +++++++++++++ srv/apps/z2ui5_cl_smp_app_189.js | 93 ++++++++++++ srv/apps/z2ui5_cl_smp_app_202.js | 119 +++++++++++++++ srv/apps/z2ui5_cl_smp_app_255.js | 168 +++++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_279.js | 141 ++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_305.js | 125 ++++++++++++++++ srv/apps/z2ui5_cl_smp_app_316.js | 173 ++++++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_325.js | 121 +++++++++++++++ srv/apps/z2ui5_cl_smp_app_327.js | 196 +++++++++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_361.js | 76 ++++++++++ srv/apps/z2ui5_cl_smp_app_382.js | 136 +++++++++++++++++ srv/apps/z2ui5_cl_smp_app_445.js | 170 +++++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_448.js | 82 +++++++++++ srv/apps/z2ui5_cl_smp_app_449.js | 75 ++++++++++ srv/apps/z2ui5_cl_smp_app_450.js | 95 ++++++++++++ srv/apps/z2ui5_cl_smp_app_456.js | 103 +++++++++++++ srv/apps/z2ui5_cl_smp_app_457.js | 69 +++++++++ srv/apps/z2ui5_cl_smp_app_460.js | 80 ++++++++++ srv/apps/z2ui5_cl_smp_app_462.js | 134 +++++++++++++++++ srv/apps/z2ui5_cl_smp_app_465.js | 83 +++++++++++ srv/apps/z2ui5_cl_smp_app_466.js | 71 +++++++++ srv/apps/z2ui5_cl_smp_app_467.js | 109 ++++++++++++++ srv/apps/z2ui5_cl_smp_app_469.js | 61 ++++++++ srv/apps/z2ui5_cl_smp_app_470.js | 174 ++++++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_472.js | 101 +++++++++++++ srv/apps/z2ui5_cl_smp_app_473.js | 81 +++++++++++ srv/apps/z2ui5_cl_smp_app_474.js | 132 +++++++++++++++++ srv/apps/z2ui5_cl_smp_app_488.js | 30 ++-- srv/apps/z2ui5_cl_smp_app_489.js | 23 +-- srv/apps/z2ui5_cl_smp_app_490.js | 104 +++++++++++++ srv/apps/z2ui5_cl_smp_app_491.js | 57 ++++++++ srv/apps/z2ui5_cl_smp_app_492.js | 67 +++++++++ srv/apps/z2ui5_cl_smp_app_493.js | 12 +- srv/apps/z2ui5_cl_smp_app_494.js | 18 +-- srv/apps/z2ui5_cl_smp_app_495.js | 22 +-- srv/apps/z2ui5_cl_smp_app_496.js | 140 ++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_498.js | 165 +++++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_499.js | 243 +++++++++++++++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_503.js | 103 +++++++++++++ srv/apps/z2ui5_cl_smp_app_506.js | 143 ++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_507.js | 130 +++++++++++++++++ srv/apps/z2ui5_cl_smp_app_508.js | 106 ++++++++++++++ srv/apps/z2ui5_cl_smp_app_509.js | 105 +++++++++++++ srv/apps/z2ui5_cl_smp_app_510.js | 193 ++++++++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_511.js | 131 +++++++++++++++++ srv/apps/z2ui5_cl_smp_app_513.js | 130 +++++++++++++++++ srv/apps/z2ui5_cl_smp_app_514.js | 148 +++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_515.js | 101 +++++++++++++ srv/apps/z2ui5_cl_smp_app_516.js | 113 ++++++++++++++ srv/apps/z2ui5_cl_smp_app_518.js | 83 +++++++++++ srv/apps/z2ui5_cl_smp_app_519.js | 100 +++++++++++++ test/differential.mjs | 106 ++++++++++++++ test/samples.test.mjs | 14 +- 81 files changed, 8118 insertions(+), 102 deletions(-) create mode 100644 ABAP2UI5_SAMPLES_PIN create mode 100644 scripts/generate.mjs create mode 100644 scripts/originals.mjs create mode 100644 scripts/samples.json create mode 100644 srv/apps/z2ui5_cl_smp_app_026.js create mode 100644 srv/apps/z2ui5_cl_smp_app_027.js create mode 100644 srv/apps/z2ui5_cl_smp_app_050.js create mode 100644 srv/apps/z2ui5_cl_smp_app_065.js create mode 100644 srv/apps/z2ui5_cl_smp_app_067.js create mode 100644 srv/apps/z2ui5_cl_smp_app_088.js create mode 100644 srv/apps/z2ui5_cl_smp_app_109.js create mode 100644 srv/apps/z2ui5_cl_smp_app_120.js create mode 100644 srv/apps/z2ui5_cl_smp_app_122.js create mode 100644 srv/apps/z2ui5_cl_smp_app_133.js create mode 100644 srv/apps/z2ui5_cl_smp_app_143.js create mode 100644 srv/apps/z2ui5_cl_smp_app_163.js create mode 100644 srv/apps/z2ui5_cl_smp_app_166.js create mode 100644 srv/apps/z2ui5_cl_smp_app_170.js create mode 100644 srv/apps/z2ui5_cl_smp_app_173.js create mode 100644 srv/apps/z2ui5_cl_smp_app_176.js create mode 100644 srv/apps/z2ui5_cl_smp_app_186.js create mode 100644 srv/apps/z2ui5_cl_smp_app_189.js create mode 100644 srv/apps/z2ui5_cl_smp_app_202.js create mode 100644 srv/apps/z2ui5_cl_smp_app_255.js create mode 100644 srv/apps/z2ui5_cl_smp_app_279.js create mode 100644 srv/apps/z2ui5_cl_smp_app_305.js create mode 100644 srv/apps/z2ui5_cl_smp_app_316.js create mode 100644 srv/apps/z2ui5_cl_smp_app_325.js create mode 100644 srv/apps/z2ui5_cl_smp_app_327.js create mode 100644 srv/apps/z2ui5_cl_smp_app_361.js create mode 100644 srv/apps/z2ui5_cl_smp_app_382.js create mode 100644 srv/apps/z2ui5_cl_smp_app_445.js create mode 100644 srv/apps/z2ui5_cl_smp_app_448.js create mode 100644 srv/apps/z2ui5_cl_smp_app_449.js create mode 100644 srv/apps/z2ui5_cl_smp_app_450.js create mode 100644 srv/apps/z2ui5_cl_smp_app_456.js create mode 100644 srv/apps/z2ui5_cl_smp_app_457.js create mode 100644 srv/apps/z2ui5_cl_smp_app_460.js create mode 100644 srv/apps/z2ui5_cl_smp_app_462.js create mode 100644 srv/apps/z2ui5_cl_smp_app_465.js create mode 100644 srv/apps/z2ui5_cl_smp_app_466.js create mode 100644 srv/apps/z2ui5_cl_smp_app_467.js create mode 100644 srv/apps/z2ui5_cl_smp_app_469.js create mode 100644 srv/apps/z2ui5_cl_smp_app_470.js create mode 100644 srv/apps/z2ui5_cl_smp_app_472.js create mode 100644 srv/apps/z2ui5_cl_smp_app_473.js create mode 100644 srv/apps/z2ui5_cl_smp_app_474.js create mode 100644 srv/apps/z2ui5_cl_smp_app_490.js create mode 100644 srv/apps/z2ui5_cl_smp_app_491.js create mode 100644 srv/apps/z2ui5_cl_smp_app_492.js create mode 100644 srv/apps/z2ui5_cl_smp_app_496.js create mode 100644 srv/apps/z2ui5_cl_smp_app_498.js create mode 100644 srv/apps/z2ui5_cl_smp_app_499.js create mode 100644 srv/apps/z2ui5_cl_smp_app_503.js create mode 100644 srv/apps/z2ui5_cl_smp_app_506.js create mode 100644 srv/apps/z2ui5_cl_smp_app_507.js create mode 100644 srv/apps/z2ui5_cl_smp_app_508.js create mode 100644 srv/apps/z2ui5_cl_smp_app_509.js create mode 100644 srv/apps/z2ui5_cl_smp_app_510.js create mode 100644 srv/apps/z2ui5_cl_smp_app_511.js create mode 100644 srv/apps/z2ui5_cl_smp_app_513.js create mode 100644 srv/apps/z2ui5_cl_smp_app_514.js create mode 100644 srv/apps/z2ui5_cl_smp_app_515.js create mode 100644 srv/apps/z2ui5_cl_smp_app_516.js create mode 100644 srv/apps/z2ui5_cl_smp_app_518.js create mode 100644 srv/apps/z2ui5_cl_smp_app_519.js create mode 100644 test/differential.mjs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 46749f2..c0f099e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,17 +13,39 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: .nvmrc - # Until cap2ui5 0.2.0 is on npm, the samples run against cap2UI5's main: - # its plugin packed as `npm publish` packs it, and installed alone - - # which brings the other dependencies along, as the README says for a - # local checkout. Once 0.2.0 is published this goes, a package-lock.json - # pins it, and `cache: npm` and npm ci come back. + # Until cap2ui5 0.2.0 is on npm, the samples run against cap2UI5's plugin: + # packed as `npm publish` packs it, and installed alone - which brings the + # other dependencies along, as the README says for a local checkout. Once + # it is published this goes, a package-lock.json pins it, and `cache: npm` + # and npm ci come back. + # + # The ref is the branch that adds abap2js (and t.numc, which + # z2ui5_cl_smp_app_067 needs) until that is on cap2UI5's main - then this + # line goes too. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: cap2UI5/cap2UI5 + ref: claude/absp2ui5-cap2ui5-transfer-7pg6dg path: .cap2ui5 persist-credentials: false - run: npm pack --pack-destination "$RUNNER_TEMP" working-directory: .cap2ui5/plugin - run: npm install --no-save --no-audit --no-fund "$RUNNER_TEMP"/cap2ui5-*.tgz - run: npm test + + # ---- the translation: srv/apps is what abap2js makes of the pinned samples + - id: pin + run: echo "sha=$(cat ABAP2UI5_SAMPLES_PIN)" >> "$GITHUB_OUTPUT" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: abap2UI5/samples + ref: ${{ steps.pin.outputs.sha }} + path: .abap2UI5-samples + persist-credentials: false + - name: srv/apps is what abap2js makes of the pinned samples + run: node scripts/generate.mjs --check --samples .abap2UI5-samples + # every sample beside its transpiled ABAP original, roundtrip for roundtrip + - name: the samples behave as their ABAP originals + run: | + node scripts/originals.mjs --samples .abap2UI5-samples + node --test test/differential.mjs diff --git a/.gitignore b/.gitignore index cf0d054..744d4ac 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ node_modules/ *.sqlite-* *.log .DS_Store + +# the differential test's transpiled originals and open-abap-core (scripts/originals.mjs) +test/.originals/ +.deps/ diff --git a/ABAP2UI5_SAMPLES_PIN b/ABAP2UI5_SAMPLES_PIN new file mode 100644 index 0000000..20724f9 --- /dev/null +++ b/ABAP2UI5_SAMPLES_PIN @@ -0,0 +1 @@ +cdd5c8b09a4ecdc51943b827ff6711d6c3af13a5 diff --git a/README.md b/README.md index 876ae9a..0122db2 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,29 @@ The [abap2UI5 samples](https://github.com/abap2UI5/samples) as [cap2UI5](https://github.com/cap2UI5/cap2UI5) apps: every sample a plain -JavaScript class in `srv/apps/`, in an ordinary CAP project - and line for -line the ABAP original. The client is abap2UI5's `z2ui5_if_client` by its own -names, the view is built with `z2ui5_cl_ui5_view_builder`, called the way ABAP -calls it: `client->check_app_prev_stack( )` is -`client.check_app_prev_stack()`. +JavaScript class in `srv/apps/`, in an ordinary CAP project - **translated from +its ABAP original, not rewritten.** cap2ui5's `abap2js` reads the ABAP class +and writes the module line for line: the same class name, the same attributes +and methods, the same calls - the client is abap2UI5's `z2ui5_if_client` by its +own names, the view is built with `z2ui5_cl_ui5_view_builder` - and the +original's comments and texts, all of them, as they are. +`client->check_app_prev_stack( )` is `client.check_app_prev_stack()`. > [!NOTE] -> **Status: first examples.** Nine of the 129 apps are ported, to agree on the -> conventions below before the rest follows. They need **cap2ui5 0.2.0**, -> which is not on npm yet (the client under its ABAP names and the view -> builder are in its `Unreleased` changelog). Until it is, a plain -> `npm install` answers `ETARGET`; install the plugin packed from a cap2UI5 -> checkout instead, which brings the other dependencies along - CI does the -> same with cap2UI5's `main`: +> **Status: 71 of the 129 samples.** 69 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 +> every one of them to its ABAP original. They need a cap2ui5 that is not on +> npm yet: 0.2.0 (the client under its ABAP names, the view builder) plus +> abap2js and `t.numc()`, which are on cap2UI5's branch +> `claude/absp2ui5-cap2ui5-transfer-7pg6dg` until they are on its `main`. +> Until then a plain `npm install` answers `ETARGET`; install the plugin packed +> from that branch instead, which brings the other dependencies along - CI does +> the same: > > ```bash -> (cd ../cap2UI5 && scripts/assemble-runtime.sh --package 1.145.0 && npm install \ -> && npm pack --workspace plugin --pack-destination /tmp) +> git clone -b claude/absp2ui5-cap2ui5-transfer-7pg6dg https://github.com/cap2UI5/cap2UI5 ../cap2UI5 +> (cd ../cap2UI5/plugin && npm pack --pack-destination /tmp) > npm install --no-save /tmp/cap2ui5-*.tgz > ``` @@ -34,46 +39,165 @@ npm test # every sample driven over the wire Log in as `alice` with an empty password (CAP's mocked development user), then open e.g. . -## The samples so far +## The samples | Sample | App | ABAP original | |---|---|---| | Basics I - Hello World, the Smallest App | [`Z2UI5_CL_SMP_APP_493`](srv/apps/z2ui5_cl_smp_app_493.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_493.clas.abap) | | Basics II - Data Binding: Input and Button | [`Z2UI5_CL_SMP_APP_494`](srv/apps/z2ui5_cl_smp_app_494.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_494.clas.abap) | | Basics III - Lifecycle: Init, Event, Navigated | [`Z2UI5_CL_SMP_APP_495`](srv/apps/z2ui5_cl_smp_app_495.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_495.clas.abap) | -| Table - Editable Cells, Add and Delete Rows | [`Z2UI5_CL_SMP_APP_011`](srv/apps/z2ui5_cl_smp_app_011.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_011.clas.abap) | +| Basics V - The Developer Tools (Ctrl+F12) | [`Z2UI5_CL_SMP_APP_496`](srv/apps/z2ui5_cl_smp_app_496.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_496.clas.abap) | +| Basics VI - Unit Tests for the App Logic | [`Z2UI5_CL_SMP_APP_503`](srv/apps/z2ui5_cl_smp_app_503.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_503.clas.abap) | +| Basics VII - Translatable Texts (Text Elements) | [`Z2UI5_CL_SMP_APP_519`](srv/apps/z2ui5_cl_smp_app_519.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_519.clas.abap) | +| Binding - Currency Amounts (sap.ui.model.type.Currency) | [`Z2UI5_CL_SMP_APP_067`](srv/apps/z2ui5_cl_smp_app_067.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_067.clas.abap) | +| Binding - Expression Binding, Types and Composite Parts | [`Z2UI5_CL_SMP_APP_027`](srv/apps/z2ui5_cl_smp_app_027.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_027.clas.abap) | +| Binding - Omit Initial Values | [`Z2UI5_CL_SMP_APP_507`](srv/apps/z2ui5_cl_smp_app_507.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_507.clas.abap) | +| Binding - Path Only (_bind_path) | [`Z2UI5_CL_SMP_APP_508`](srv/apps/z2ui5_cl_smp_app_508.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_508.clas.abap) | +| Binding - Pre-serialized JSON (json) | [`Z2UI5_CL_SMP_APP_509`](srv/apps/z2ui5_cl_smp_app_509.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_509.clas.abap) | +| Binding - Structure Fields and INCLUDEs | [`Z2UI5_CL_SMP_APP_166`](srv/apps/z2ui5_cl_smp_app_166.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_166.clas.abap) | +| Browser - Copy to Clipboard | [`Z2UI5_CL_SMP_APP_325`](srv/apps/z2ui5_cl_smp_app_325.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_325.clas.abap) | +| Browser - Keyboard Layout of an Input (inputmode) | [`Z2UI5_CL_SMP_APP_516`](srv/apps/z2ui5_cl_smp_app_516.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_516.clas.abap) | +| Browser - Local and Session Storage | [`Z2UI5_CL_SMP_APP_327`](srv/apps/z2ui5_cl_smp_app_327.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_327.clas.abap) | +| Browser - Logout from the Client | [`Z2UI5_CL_SMP_APP_361`](srv/apps/z2ui5_cl_smp_app_361.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_361.clas.abap) | +| Browser - Open Mail, Phone and SMS Links | [`Z2UI5_CL_SMP_APP_316`](srv/apps/z2ui5_cl_smp_app_316.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_316.clas.abap) | +| Browser - Reload the Page | [`Z2UI5_CL_SMP_APP_492`](srv/apps/z2ui5_cl_smp_app_492.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_492.clas.abap) | +| Browser - Set the Tab Favicon | [`Z2UI5_CL_SMP_APP_491`](srv/apps/z2ui5_cl_smp_app_491.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_491.clas.abap) | +| Browser - Set the Tab Title | [`Z2UI5_CL_SMP_APP_125`](srv/apps/z2ui5_cl_smp_app_125.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_125.clas.abap) | +| 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) | +| 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) | +| Control Behaviour - Open the PDF Viewer by ID | [`Z2UI5_CL_SMP_APP_449`](srv/apps/z2ui5_cl_smp_app_449.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_449.clas.abap) | +| Control Behaviour - Register an Icon Font | [`Z2UI5_CL_SMP_APP_518`](srv/apps/z2ui5_cl_smp_app_518.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_518.clas.abap) | +| Control Behaviour - Switch NavContainer Page by ID | [`Z2UI5_CL_SMP_APP_088`](srv/apps/z2ui5_cl_smp_app_088.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_088.clas.abap) | +| Control Behaviour - The Global Busy Indicator | [`Z2UI5_CL_SMP_APP_515`](srv/apps/z2ui5_cl_smp_app_515.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_515.clas.abap) | +| Control Behaviour - Wizard with Steps | [`Z2UI5_CL_SMP_APP_202`](srv/apps/z2ui5_cl_smp_app_202.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_202.clas.abap) | +| 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 - 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) | +| Event - Literal Arguments (check_arg_literal) | [`Z2UI5_CL_SMP_APP_506`](srv/apps/z2ui5_cl_smp_app_506.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_506.clas.abap) | +| File - Download to the Browser | [`Z2UI5_CL_SMP_APP_186`](srv/apps/z2ui5_cl_smp_app_186.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_186.clas.abap) | +| Focus - Jump to the Next Input on Enter | [`Z2UI5_CL_SMP_APP_189`](srv/apps/z2ui5_cl_smp_app_189.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_189.clas.abap) | +| Focus - Set Focus and Select Text in an Input | [`Z2UI5_CL_SMP_APP_133`](srv/apps/z2ui5_cl_smp_app_133.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_133.clas.abap) | +| Formatter - ABAP Date and Time Strings (DATS/TIMS) | [`Z2UI5_CL_SMP_APP_450`](srv/apps/z2ui5_cl_smp_app_450.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_450.clas.abap) | +| Formatter - Date Object for the DatePicker | [`Z2UI5_CL_SMP_APP_457`](srv/apps/z2ui5_cl_smp_app_457.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_457.clas.abap) | +| Formatter - Date Objects for the PlanningCalendar | [`Z2UI5_CL_SMP_APP_456`](srv/apps/z2ui5_cl_smp_app_456.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_456.clas.abap) | +| Formatter - Inline Icons in a Text | [`Z2UI5_CL_SMP_APP_466`](srv/apps/z2ui5_cl_smp_app_466.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_466.clas.abap) | +| Grid Table - Keep Column Filters on Refresh | [`Z2UI5_CL_SMP_APP_143`](srv/apps/z2ui5_cl_smp_app_143.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_143.clas.abap) | +| Hash - App State, Bookmark and Share | [`Z2UI5_CL_SMP_APP_498`](srv/apps/z2ui5_cl_smp_app_498.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_498.clas.abap) | +| Hash - App-Owned Routing (#/detail) | [`Z2UI5_CL_SMP_APP_499`](srv/apps/z2ui5_cl_smp_app_499.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_499.clas.abap) | +| Menu - Full Path of the Selected Item | [`Z2UI5_CL_SMP_APP_473`](srv/apps/z2ui5_cl_smp_app_473.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_473.clas.abap) | +| Menu - Menu as Popover with core:require | [`Z2UI5_CL_SMP_APP_163`](srv/apps/z2ui5_cl_smp_app_163.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_163.clas.abap) | +| Message - Message Model and MessageManager | [`Z2UI5_CL_SMP_APP_467`](srv/apps/z2ui5_cl_smp_app_467.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_467.clas.abap) | +| Message - MessageBox, Types and Custom Actions | [`Z2UI5_CL_SMP_APP_382`](srv/apps/z2ui5_cl_smp_app_382.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_382.clas.abap) | +| Message - MessagePopover URL Policy | [`Z2UI5_CL_SMP_APP_474`](srv/apps/z2ui5_cl_smp_app_474.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_474.clas.abap) | +| Navigation - Data Input App | [`Z2UI5_CL_SMP_APP_489`](srv/apps/z2ui5_cl_smp_app_489.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_489.clas.abap) | +| 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) | +| 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) | +| Popover - Open Together with the View Build | [`Z2UI5_CL_SMP_APP_490`](srv/apps/z2ui5_cl_smp_app_490.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_490.clas.abap) | +| Popover - QuickView Contact Card | [`Z2UI5_CL_SMP_APP_109`](srv/apps/z2ui5_cl_smp_app_109.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_109.clas.abap) | +| Popover - Toggle by ID (toggleBy) | [`Z2UI5_CL_SMP_APP_465`](srv/apps/z2ui5_cl_smp_app_465.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_465.clas.abap) | | 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) | -| Navigation - Return Data and Events to the Caller | [`Z2UI5_CL_SMP_APP_488`](srv/apps/z2ui5_cl_smp_app_488.js) (+ [`_489`](srv/apps/z2ui5_cl_smp_app_489.js)) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_488.clas.abap) | -| Browser - Set the Tab Title | [`Z2UI5_CL_SMP_APP_125`](srv/apps/z2ui5_cl_smp_app_125.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_125.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) | +| 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) | +| 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) | + +## How a sample gets here + +[`scripts/samples.json`](scripts/samples.json) names every sample and how it +is made: + +- **`generated`** - `npm run generate` reads the class from an abap2UI5/samples + checkout at the pin and writes what + [abap2js](https://github.com/cap2UI5/cap2UI5/blob/main/plugin/README.md#an-abap-app-translated-npx-cap2ui5-abap2js) + makes of it into `srv/apps/.js`. **These files are never edited by + hand:** a change goes into the ABAP sample, the pin moves, and + `npm run generate` runs again. `npm run check:generated` - a CI gate - fails + when a file is not what the translation writes. +- **`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. + +The checkout is `--samples `, else `$ABAP2UI5_SAMPLES`, else +`../abap2UI5-samples`, and it has to be at the pin - `@origin` in a module +names the source it really came from: + +```bash +git clone https://github.com/abap2UI5/samples ../abap2UI5-samples +git -C ../abap2UI5-samples checkout "$(cat ABAP2UI5_SAMPLES_PIN)" +npm run generate +``` + +**Adding a sample:** its class name into `generated`, then `npm run generate`. +abap2js translates it, or refuses with file, row, column and the construct - +then the sample waits for abap2js, or is ported by hand and listed under +`handwritten`. Either way it passes the differential test before it is +committed. **Moving the pin:** the new commit into `ABAP2UI5_SAMPLES_PIN` and +the checkout, `npm run generate`, `npm run differential` - and a hand-written +port follows its original's diff by hand. + +## The differential test + +"Line for line" is a claim about the source; `npm run differential` is the one +about what the source *does*. It transpiles every sample's ABAP original with +@abaplint/transpiler - the version @abap2ui5/node-runtime was built with, +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: +`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. + +It needs the network once (open-abap-core is cloned into `.deps/`) and +`@abaplint/transpiler-cli` at the runtime's version, a devDependency. -## How a sample is ported +## What a translated sample looks like -**1:1, so that it is recognized at a glance.** Put the ABAP original and the -port side by side and every line has its counterpart: +Put the ABAP original and the module side by side and every line has its +counterpart: - **One ABAP class, one file, the same name.** `z2ui5_cl_smp_app_493.clas.abap` becomes `srv/apps/z2ui5_cl_smp_app_493.js`, registered as - `Z2UI5_CL_SMP_APP_493` - so `?app_start=` is the same on both sides, a + `Z2UI5_CL_SMP_APP_493` - so `?app_start=` is the same on both sides, and a sample that calls another one keeps calling it by its name. The header keeps the original's `@keywords`, `@summary` and `@docs`, and `@origin` points back at it. - **The same class.** The same attributes under the same names, the same - methods (`view_display( )`, `on_event( )`, …) in the same order, the same - dispatcher in `main( )`, `me->client = client` as `this.client = client`. + methods in the same order, the same dispatcher in `main( )`, + `me->client = client` as `this.client = client`. - **The same calls.** The client's methods and the view builder's are the ABAP ones; a method's preferred parameter is its one positional argument and - parameters by name are one object with the ABAP names. The view keeps the - original's tree and layout: one call per line, four spaces per tree level, - `end()` in the column of the `ele()` it closes, the `v =` column aligned. -- **The same texts.** Only the page title says `cap2UI5 - …`, and a text that - names an ABAP construct (`z2ui5_if_app`, `client->…`) names the JavaScript - one. + parameters by name are one object with the ABAP names. A statement starts + where the ABAP one starts and breaks where it breaks - the view keeps the + original's tree, one call per line. +- **The same texts, all of them.** The page title says `abap2UI5 - …` as the + original does, a text that names an ABAP construct (`z2ui5_if_app`, + `client->…`) still names it, and every comment comes along. | abap2UI5 | cap2UI5 | |---|---| -| `DATA name TYPE string.` | `name = "";` - the initial value is the type | -| `DATA t_tab TYPE STANDARD TABLE OF ty_s_row WITH EMPTY KEY.` | `t_tab = t.table({ …one row… });` | +| `DATA name TYPE string.`, `DATA flag TYPE abap_bool.` | `name = "";`, `flag = false;` - the initial value is the type | +| `DATA amount TYPE p LENGTH 14 DECIMALS 3.` | `amount = t.packed(14, 3);` - and `t.numc( )`, `t.date( )`, `t.time( )` | +| `TYPES: BEGIN OF ty_s_row, … END OF ty_s_row.` | `const ty_s_row = { … };` - the row's initial values | +| `DATA t_tab TYPE STANDARD TABLE OF ty_s_row WITH EMPTY KEY.` | `t_tab = t.table(ty_s_row);` | | `METHOD z2ui5_if_app~main.` | `main(client) {` | | `me->client = client.` / `client->…` in a method | `this.client = client;` / `this.client.…` | | `IF client->check_on_navigated( ).` | `if (client.check_on_navigated()) {` | @@ -86,17 +210,19 @@ port side by side and every line has its counterpart: | `z2ui5_if_client=>cs_event-set_title` | `z2ui5_if_client.cs_event.set_title` | | `client->nav_app_call( NEW z2ui5_cl_smp_app_493( ) )` | `client.nav_app_call("Z2UI5_CL_SMP_APP_493")` | | `client->nav_app_leave( event = … r_data = … )` | `client.nav_app_leave({ event: …, r_data: … })` | -| `ASSIGN client->get( )-r_event_data->* TO …` | `client.get().r_event_data` - the data itself | | ``CASE client->get_event( ). WHEN `A` OR `B`.`` | `switch (client.get_event()) { case "A": case "B":` | +| `INSERT row INTO TABLE t_tab.` | `this.t_tab = [...this.t_tab, row];` | +| ``s_struc-title = `x`.`` | `this.s_struc = { ...this.s_struc, title: "x" };` | +| `\|Hello { name }\|`, `a && b` | `` `Hello ${this.name}` ``, `a + b` | +| `COND #( WHEN a THEN b ELSE c )` | `a ? b : c` | ### What differs from ABAP 1. **A field is bound by its name**, `client._bind("name")`: ABAP's `_bind( )` finds the attribute by reference, which a JavaScript value cannot carry. -2. **`this` reads plain copies.** Assign to write: - `this.t_tab = [...this.t_tab, row]` for `INSERT … INTO TABLE t_tab`. A - `push` on the copy is lost. Assigning an object replaces a structure, so - `this.s_result = {}` is `s_result = VALUE #( )`. +2. **`this` reads plain copies.** Assign to write: `INSERT … INTO TABLE t_tab` + is `this.t_tab = [...this.t_tab, row]`, a component is written by assigning + the structure. A `push` on the copy is lost. 3. **Every field is part of the model.** ABAP keeps a `PROTECTED` attribute out of it; a JavaScript field with an initial value is always bound. The client is the exception: assigned in `main( )` and not declared, it stays out. diff --git a/package.json b/package.json index 16ba91c..2fbe4de 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,18 @@ "cap2ui5": "^0.2.0" }, "devDependencies": { + "@abaplint/transpiler-cli": "2.13.91", "@cap-js/cds-test": "^1.0.2", "fast-xml-parser": "^5.11.1" }, "scripts": { "start": "cds-serve", "watch": "cds watch", - "test": "node --test test/*.test.mjs" + "test": "node --test test/*.test.mjs", + "generate": "node scripts/generate.mjs", + "check:generated": "node scripts/generate.mjs --check", + "originals": "node scripts/originals.mjs", + "differential": "node scripts/originals.mjs && node --test test/differential.mjs" }, "cds": { "requires": { diff --git a/scripts/generate.mjs b/scripts/generate.mjs new file mode 100644 index 0000000..d2252c4 --- /dev/null +++ b/scripts/generate.mjs @@ -0,0 +1,79 @@ +#!/usr/bin/env node +// generate - the abap2UI5 samples as cap2UI5 apps, translated, not ported. +// +// node scripts/generate.mjs [--check] [--samples ] +// +// Every class scripts/samples.json lists under "generated" is read from an +// abap2UI5/samples checkout AT the commit ABAP2UI5_SAMPLES_PIN names and +// translated by cap2ui5's abap2js into srv/apps/.js - the same class +// name, so ?app_start= is the same on both sides, and the original's lines, +// comments and texts. Nothing in those files is edited by hand: a change goes +// into the ABAP sample, the pin moves, and this runs again. +// +// The checkout is --samples, else $ABAP2UI5_SAMPLES, else ../abap2UI5-samples. +// It has to be at the pin, so that `@origin` in a module names the source the +// module really came from. +// +// --check writes nothing and exits 1 when a generated module is missing or +// differs from what the translation writes - the CI gate that keeps srv/apps +// and the pin in step. The HAND-WRITTEN ports (samples.json "handwritten") are +// never touched. +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { abap2js, Abap2jsError } from "cap2ui5"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const args = process.argv.slice(2); +const check = args.includes("--check"); +const at = args.indexOf("--samples"); +const checkout = path.resolve(at >= 0 ? args[at + 1] : process.env.ABAP2UI5_SAMPLES ?? path.join(ROOT, "..", "abap2UI5-samples")); + +const pin = fs.readFileSync(path.join(ROOT, "ABAP2UI5_SAMPLES_PIN"), "utf8").trim(); +const { generated, handwritten } = JSON.parse(fs.readFileSync(path.join(ROOT, "scripts", "samples.json"), "utf8")); +const src = path.join(checkout, "src"); + +if (!fs.existsSync(src)) { + console.error(`generate: 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); +} +let head = ""; +try { head = execFileSync("git", ["-C", checkout, "rev-parse", "HEAD"], { encoding: "utf8" }).trim(); } catch { /* no git */ } +if (head !== pin) { + console.error(`generate: ${checkout} is at ${head || "an unknown commit"}, ABAP2UI5_SAMPLES_PIN says ${pin}`); + process.exit(1); +} +const overlap = generated.filter((n) => n in handwritten); +if (overlap.length) { + console.error(`generate: ${overlap.join(", ")} listed as generated AND handwritten in scripts/samples.json`); + process.exit(1); +} + +let failed = 0; +let changed = 0; +for (const name of generated) { + const file = path.join(src, `${name}.clas.abap`); + let code; + try { + ({ code } = abap2js(fs.readFileSync(file, "utf8"), { + file, lib: [src], origin: `abap2UI5/samples src/${name}.clas.abap`, format: "esm", + })); + } catch (e) { + if (!(e instanceof Abap2jsError)) throw e; + console.error(`refused: ${e.message}`); + failed++; + continue; + } + const target = path.join(ROOT, "srv", "apps", `${name}.js`); + const before = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : null; + if (before === code) continue; + changed++; + if (check) console.error(`${before === null ? "missing" : "differs"}: srv/apps/${name}.js`); + else fs.writeFileSync(target, code); +} + +console.log(`generate: ${generated.length} samples from abap2UI5/samples@${pin.slice(0, 7)}, ` + + `${changed} ${check ? "to regenerate" : "written"}, ${failed} refused`); +process.exit(failed || (check && changed) ? 1 : 0); diff --git a/scripts/originals.mjs b/scripts/originals.mjs new file mode 100644 index 0000000..980e1fd --- /dev/null +++ b/scripts/originals.mjs @@ -0,0 +1,88 @@ +#!/usr/bin/env node +// originals - the ABAP originals of this repository's samples, transpiled, for +// the differential test (test/differential.mjs). +// +// node scripts/originals.mjs [--samples ] +// +// Every class scripts/samples.json names is copied from the checkout at the +// pin, renamed Z2UI5_CL_SMP_APP_nnn -> ZABAP_SMP_APP_nnn so that original and +// translation can be served side by side, and transpiled by +// @abaplint/transpiler - the version @abap2ui5/node-runtime was built with, +// against the ABAP that package ships in downport/ - into test/.originals/. +// The runtime's own transpiler output is what cap2UI5 hosts, so the original +// runs on exactly the framework the translation runs on. +// +// Needs the network once: open-abap-core is cloned into .deps/. +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import { createRequire } from "node:module"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const require = createRequire(import.meta.url); +const args = process.argv.slice(2); +const at = args.indexOf("--samples"); +const checkout = path.resolve(at >= 0 ? args[at + 1] : process.env.ABAP2UI5_SAMPLES ?? path.join(ROOT, "..", "abap2UI5-samples")); +const pin = fs.readFileSync(path.join(ROOT, "ABAP2UI5_SAMPLES_PIN"), "utf8").trim(); +const { generated, handwritten } = JSON.parse(fs.readFileSync(path.join(ROOT, "scripts", "samples.json"), "utf8")); +const names = [...generated, ...Object.keys(handwritten)]; + +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}`); + +// the transpiler the runtime was built with: its output is tied to the runtime +const runtimeDir = path.dirname(require.resolve("@abap2ui5/node-runtime/package.json", { paths: [require.resolve("cap2ui5")] })); +const wanted = JSON.parse(fs.readFileSync(path.join(runtimeDir, "package.json"), "utf8")).abap2ui5?.transpiler; +const have = JSON.parse(fs.readFileSync(require.resolve("@abaplint/transpiler-cli/package.json"), "utf8")).version; +if (wanted && wanted !== have) { + throw new Error(`@abaplint/transpiler-cli ${have} is installed, @abap2ui5/node-runtime was built with ${wanted} - ` + + `npm i -D --save-exact @abaplint/transpiler-cli@${wanted}`); +} + +const deps = path.join(ROOT, ".deps", "open-abap-core"); +if (!fs.existsSync(deps)) { + execFileSync("git", ["clone", "--depth", "1", "https://github.com/open-abap/open-abap-core", deps], { stdio: "inherit" }); +} + +const work = fs.mkdtempSync(path.join(os.tmpdir(), "originals-")); +try { + const input = path.join(work, "abap"); + fs.mkdirSync(input); + const rename = (s) => s.replace(/z2ui5_cl_smp_app_(\d{3})/g, "zabap_smp_app_$1").replace(/Z2UI5_CL_SMP_APP_(\d{3})/g, "ZABAP_SMP_APP_$1"); + for (const name of names) { + for (const ext of ["clas.abap", "clas.xml"]) { + const file = path.join(checkout, "src", `${name}.${ext}`); + if (fs.existsSync(file)) fs.writeFileSync(path.join(input, `${rename(name)}.${ext}`), rename(fs.readFileSync(file, "utf8"))); + } + } + // the transpiler reads a lib folder relative to where it runs + fs.symlinkSync(path.join(runtimeDir, "downport"), path.join(work, "downport")); + fs.symlinkSync(deps, path.join(work, "open-abap-core")); + fs.writeFileSync(path.join(work, "abap_transpile.json"), JSON.stringify({ + input_folder: "abap", + output_folder: "output", + libs: [ + { folder: "/downport", files: "/**/*.*" }, + { folder: "/open-abap-core" }, + ], + write_unit_tests: false, + options: { ignoreSyntaxCheck: false, addFilenames: true, unknownTypes: "runtimeError" }, + }, null, 2)); + execFileSync(process.execPath, [require.resolve("@abaplint/transpiler-cli/abap_transpile"), "abap_transpile.json"], + { cwd: work, stdio: ["ignore", "ignore", "inherit"] }); + const out = path.join(ROOT, "test", ".originals"); + fs.rmSync(out, { recursive: true, force: true }); + fs.mkdirSync(out, { recursive: true }); + let n = 0; + for (const name of names) { + const file = path.join(work, "output", `${rename(name)}.clas.mjs`); + if (!fs.existsSync(file)) throw new Error(`the transpiler wrote no ${rename(name)}.clas.mjs`); + fs.copyFileSync(file, path.join(out, path.basename(file))); + n++; + } + console.log(`originals: ${n} classes from abap2UI5/samples@${pin.slice(0, 7)} transpiled into test/.originals/`); +} finally { + fs.rmSync(work, { recursive: true, force: true }); +} diff --git a/scripts/samples.json b/scripts/samples.json new file mode 100644 index 0000000..573bbaf --- /dev/null +++ b/scripts/samples.json @@ -0,0 +1,78 @@ +{ + "$comment": "Which abap2UI5 samples are cap2UI5 apps, and how. generated: translated by cap2ui5 abap2js from the abap2UI5/samples commit in ABAP2UI5_SAMPLES_PIN (scripts/generate.mjs) - never edited by hand. handwritten: ported by hand, because abap2js refuses a construct the class needs; the reason is what it refuses. A sample moves from handwritten to generated when abap2js learns the construct and the differential test holds it to its original.", + "generated": [ + "z2ui5_cl_smp_app_026", + "z2ui5_cl_smp_app_027", + "z2ui5_cl_smp_app_050", + "z2ui5_cl_smp_app_065", + "z2ui5_cl_smp_app_067", + "z2ui5_cl_smp_app_088", + "z2ui5_cl_smp_app_109", + "z2ui5_cl_smp_app_120", + "z2ui5_cl_smp_app_122", + "z2ui5_cl_smp_app_125", + "z2ui5_cl_smp_app_133", + "z2ui5_cl_smp_app_143", + "z2ui5_cl_smp_app_161", + "z2ui5_cl_smp_app_163", + "z2ui5_cl_smp_app_166", + "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", + "z2ui5_cl_smp_app_255", + "z2ui5_cl_smp_app_279", + "z2ui5_cl_smp_app_305", + "z2ui5_cl_smp_app_316", + "z2ui5_cl_smp_app_325", + "z2ui5_cl_smp_app_327", + "z2ui5_cl_smp_app_361", + "z2ui5_cl_smp_app_382", + "z2ui5_cl_smp_app_445", + "z2ui5_cl_smp_app_448", + "z2ui5_cl_smp_app_449", + "z2ui5_cl_smp_app_450", + "z2ui5_cl_smp_app_456", + "z2ui5_cl_smp_app_457", + "z2ui5_cl_smp_app_460", + "z2ui5_cl_smp_app_462", + "z2ui5_cl_smp_app_465", + "z2ui5_cl_smp_app_466", + "z2ui5_cl_smp_app_467", + "z2ui5_cl_smp_app_469", + "z2ui5_cl_smp_app_470", + "z2ui5_cl_smp_app_472", + "z2ui5_cl_smp_app_473", + "z2ui5_cl_smp_app_474", + "z2ui5_cl_smp_app_489", + "z2ui5_cl_smp_app_490", + "z2ui5_cl_smp_app_491", + "z2ui5_cl_smp_app_492", + "z2ui5_cl_smp_app_493", + "z2ui5_cl_smp_app_494", + "z2ui5_cl_smp_app_495", + "z2ui5_cl_smp_app_496", + "z2ui5_cl_smp_app_498", + "z2ui5_cl_smp_app_499", + "z2ui5_cl_smp_app_503", + "z2ui5_cl_smp_app_506", + "z2ui5_cl_smp_app_507", + "z2ui5_cl_smp_app_508", + "z2ui5_cl_smp_app_509", + "z2ui5_cl_smp_app_510", + "z2ui5_cl_smp_app_511", + "z2ui5_cl_smp_app_513", + "z2ui5_cl_smp_app_514", + "z2ui5_cl_smp_app_515", + "z2ui5_cl_smp_app_516", + "z2ui5_cl_smp_app_518", + "z2ui5_cl_smp_app_519" + ], + "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_488": "ASSIGN ls_get-r_event_data->* TO FIELD-SYMBOL( ) (abap2js: not supported yet)" + } +} diff --git a/srv/apps/z2ui5_cl_smp_app_011.js b/srv/apps/z2ui5_cl_smp_app_011.js index 68ddc9e..f77a150 100644 --- a/srv/apps/z2ui5_cl_smp_app_011.js +++ b/srv/apps/z2ui5_cl_smp_app_011.js @@ -29,7 +29,7 @@ defineApp("Z2UI5_CL_SMP_APP_011", class { .a({ n: "xmlns:mvc", v: "sap.ui.core.mvc" }); const page = view.ele("Shell") .ele("Page") - .a({ n: "title", v: "cap2UI5 - Table - Editable Cells, Add and Delete Rows" }) + .a({ n: "title", v: "abap2UI5 - Table - Editable Cells, Add and Delete Rows" }) .a({ n: "showNavButton", b: this.client.check_app_prev_stack() }) .a({ n: "navButtonPress", v: this.client._event_nav_app_leave() }) .a({ n: "id", v: "test2" }); diff --git a/srv/apps/z2ui5_cl_smp_app_026.js b/srv/apps/z2ui5_cl_smp_app_026.js new file mode 100644 index 0000000..2cacadd --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_026.js @@ -0,0 +1,138 @@ +// @keywords placement anchor button confirm cancel popover_display +// @summary A Popover anchored to the control that opened it, with the placements to choose from and a confirm/cancel footer. +// @docs https://abap2ui5.github.io/docs/cookbook/popup_popover/popover +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_026.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_026", class { + + placement = ""; + input = ""; + + main(client) { + + this.client = client; + + if (client.check_on_init()) { + + this.placement = "Left"; + this.input = "abcd"; + this.view_display(); + + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event("POPOVER")) { + this.popover_display("TEST"); + + } else if (client.check_on_event("BUTTON_CONFIRM")) { + + client.message_toast_display(`confirm - input: ${this.input}`); + client.popover_destroy(); + + } else if (client.check_on_event("BUTTON_CANCEL")) { + + client.message_toast_display("cancel"); + client.popover_destroy(); + + } + + } + + popover_display(id) { + + const view = 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" }); + view.ele("Popover") + .a({ n: "title", v: "Popover Title" }) + .a({ n: "placement", t: this.placement }) + .ele("footer") + .ele("OverflowToolbar") + .tag("ToolbarSpacer") + .tag("Button") + .a({ n: "press", v: this.client._event("BUTTON_CANCEL") }) + .a({ n: "text", v: "Cancel" }) + .tag("Button") + .a({ n: "press", v: this.client._event("BUTTON_CONFIRM") }) + .a({ n: "text", v: "Confirm" }) + .a({ n: "type", v: "Emphasized" }) + .end() + .end() + .tag("Text") + .a({ n: "text", v: "make an input here:" }) + .tag("Input") + .a({ n: "value", v: this.client._bind("input") }); + + this.client.popover_display({ xml: view.stringify(), by_id: id }); + + } + + 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:form", v: "sap.ui.layout.form" }); + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Popover - Basic Example with Placement" }) + .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: "Popover demo: choose a placement with the segmented button, then open a popover " + + "anchored to a control, with confirm and cancel actions." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Popover" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }) + .tag("Title") + .a({ n: "text", v: "Input" }) + .tag("Label") + .a({ n: "text", v: "Link" }) + .tag("Link") + .a({ n: "text", v: "Documentation UI5 Popover Control" }) + .a({ n: "href", v: "https://sdk.openui5.org/entity/sap.m.Popover" }) + .a({ n: "target", v: "_blank" }) + .tag("Label") + .a({ n: "text", v: "placement" }) + .ele("SegmentedButton") + .a({ n: "selectedKey", v: this.client._bind("placement") }) + .ele("items") + .tag("SegmentedButtonItem") + .a({ n: "icon", v: "sap-icon://add-favorite" }) + .a({ n: "key", v: "Left" }) + .a({ n: "text", v: "Left" }) + .tag("SegmentedButtonItem") + .a({ n: "icon", v: "sap-icon://accept" }) + .a({ n: "key", v: "Top" }) + .a({ n: "text", v: "Top" }) + .tag("SegmentedButtonItem") + .a({ n: "icon", v: "sap-icon://accept" }) + .a({ n: "key", v: "Bottom" }) + .a({ n: "text", v: "Bottom" }) + .tag("SegmentedButtonItem") + .a({ n: "icon", v: "sap-icon://attachment" }) + .a({ n: "key", v: "Right" }) + .a({ n: "text", v: "Right" }) + .end() + .end() + .tag("Label") + .a({ n: "text", v: "popover" }) + .tag("Button") + .a({ n: "press", v: this.client._event("POPOVER") }) + .a({ n: "text", v: "show" }) + .a({ n: "id", v: "TEST" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_027.js b/srv/apps/z2ui5_cl_smp_app_027.js new file mode 100644 index 0000000..5d7e477 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_027.js @@ -0,0 +1,113 @@ +// @keywords formatter parts conditional regexp visible enabled syntax +// @summary Expression binding in the view - conditions, composite parts and a regular expression decide visible and enabled without asking the backend. +// @docs https://abap2ui5.github.io/docs/cookbook/model/expression_binding +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_027.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_027", class { + + product = ""; + quantity = 0; + input2 = ""; + input31 = 0; + input32 = 0; + input41 = ""; + input51 = ""; + input52 = ""; + + main(client) { + + this.client = client; + if (client.check_on_init()) { + + this.product = "tomato"; + this.quantity = "500"; + this.input41 = "faasdfdfsaVIp"; + this.view_display(); + + } else if (client.check_on_navigated()) { + this.view_display(); + } + + } + + 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:form", v: "sap.ui.layout.form" }); + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Binding - Expression Binding, Types and Composite Parts" }) + .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: "Advanced binding syntax: expression binding, typed bindings, conditional enabling " + + "with RegExp checks, and composite (parts) bindings." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + const form = page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Binding Syntax" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }); + + form.tag("Title") + .a({ n: "text", v: "Expression Binding" }) + .tag("Label") + .a({ n: "text", v: "Documentation" }) + .tag("Link") + .a({ n: "text", v: "Expression Binding" }) + .a({ n: "href", v: "https://sdk.openui5.org/topic/daf6852a04b44d118963968a1239d2c0" }) + .a({ n: "target", v: "_blank" }) + .tag("Label") + .a({ n: "text", v: "input in uppercase" }) + .tag("Input") + .a({ n: "value", v: this.client._bind("input2") }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: `{= $${this.client._bind("input2")}.toUpperCase() }` }) + .tag("Label") + .a({ n: "text", v: "max value of the first two inputs" }) + .tag("Input") + .a({ n: "value", v: `{ type : "sap.ui.model.type.Integer", path:"${this.client._bind({ val: "input31", path: true })}" }` }) + .tag("Input") + .a({ n: "value", v: `{ type : "sap.ui.model.type.Integer", path:"${this.client._bind({ val: "input32", path: true })}" }` }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: `{= Math.max($${this.client._bind("input31")}, $${this.client._bind("input32")}) }` }) + .tag("Label") + .a({ n: "text", v: "only enabled when the quantity equals 500" }) + .tag("Input") + .a({ n: "value", v: `{ type : "sap.ui.model.type.Integer", path:"${this.client._bind({ val: "quantity", path: true })}" }` }) + .tag("Input") + .a({ n: "enabled", v: `{= 500===$${this.client._bind("quantity")} }` }) + .a({ n: "value", v: this.client._bind("product") }) + .tag("Label") + .a({ n: "text", v: "RegExp Set to enabled if the input contains VIP, ignoring the case." }) + .tag("Input") + .a({ n: "value", v: this.client._bind("input41") }) + .tag("Button") + .a({ n: "text", v: "VIP" }) + .a({ n: "enabled", v: `{= RegExp('vip', 'i').test($${this.client._bind("input41")}) }` }) + .tag("Label") + .a({ n: "text", v: "concatenate both inputs" }) + .tag("Input") + .a({ n: "value", v: this.client._bind("input51") }) + .tag("Input") + .a({ n: "value", v: this.client._bind("input52") }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: `{ parts: [ "${this.client._bind({ val: "input51", path: true })}", ` + + `"${this.client._bind({ val: "input52", path: true })}" ] }` }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_050.js b/srv/apps/z2ui5_cl_smp_app_050.js new file mode 100644 index 0000000..ec4b459 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_050.js @@ -0,0 +1,105 @@ +// @keywords style stylesheet inline html class own design +// @summary Ships a stylesheet with the view, so an app can carry its own design without a change to the UI5 theme. +// @docs https://abap2ui5.github.io/docs/cookbook/view/definition +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_050.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_050", class { + + quantity = ""; + + product = ""; + + main(client) { + + this.client = client; + if (client.check_on_init()) { + + this.product = "tomato"; + this.quantity = "500"; + this.view_display(); + + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event("BUTTON_POST")) { + client.message_toast_display(`${this.product} ${this.quantity} - send to the server`); + } + + } + + 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" }) + .a({ n: "xmlns:form", v: "sap.ui.layout.form" }); + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - CSS - Ship Your Own CSS with the View" }) + .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: "An inline html style element is sent to the frontend with the view, so the sample can restyle " + + "standard UI5 controls - here the inputs are enlarged and the post button turns red." }) + .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. + // The first rules restyle UI5's own classes, the last one is an own class + // that the button below carries in its class attribute + page.tag({ n: "HTML", ns: "core" }) + .a({ n: "content", v: "" }) + .ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Form Title" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }) + .tag("Title") + .a({ n: "text", v: "Input" }) + .tag("Label") + .a({ n: "text", v: "quantity" }) + .tag("Input") + .a({ n: "value", v: this.client._bind("quantity") }) + .tag("Label") + .a({ n: "text", v: "product" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", t: this.product }) + .tag("Button") + .a({ n: "press", v: this.client._event("BUTTON_POST") }) + .a({ n: "text", v: "post" }) + .a({ n: "class", v: "mySuperRedButton" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_065.js b/srv/apps/z2ui5_cl_smp_app_065.js new file mode 100644 index 0000000..d81c8e4 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_065.js @@ -0,0 +1,115 @@ +// @keywords nest_view_display rerender model refresh sub view +// @summary A nested view: nest_view_display renders a second view inside the first, and shows which model refresh reaches it. +// @docs https://abap2ui5.github.io/docs/cookbook/view/nested_views +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_065.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_065", class { + + mv_input_main = ""; + mv_input_nest = ""; + + mv_count = 0; + + main(client) { + + this.client = client; + if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + on_event() { + + switch (this.client.get_event()) { + case "TEST": + this.client.message_box_display(`input ${this.mv_input_nest}`); + break; + case "ALL": + this.view_display(); + this.nest_view_display(); + break; + case "MAIN": + this.view_display(); + break; + case "NEST": + this.nest_view_display(); + break; + case "NEST_MODEL": + // change only a nest-bound field, without re-rendering the nested XML. + // The main and nested views share one model and that model is pushed + // with every response, so the nested view picks the change up too. + // Press "Rerender only nested view" first so the nested view exists. + this.mv_count = this.mv_count + 1; + this.mv_input_nest = `nest model updated #${this.mv_count}`; + break; + } + + } + + view_display() { + + const lo_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" }); + + const page = lo_view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Nested View - Basic Example (nest_view_display)" }) + .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" }); + + page.tag("MessageStrip") + .a({ n: "text", v: "A main view with a nested view inside: the buttons re-render everything, only the " + + "main view, only the nested view, or refresh just the nested view's model." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele("content") + .tag("Button") + .a({ n: "press", v: this.client._event("ALL") }) + .a({ n: "text", v: "Rerender all" }) + .tag("Button") + .a({ n: "press", v: this.client._event("MAIN") }) + .a({ n: "text", v: "Rerender Main without nest" }) + .tag("Button") + .a({ n: "press", v: this.client._event("NEST") }) + .a({ n: "text", v: "Rerender only nested view" }) + .tag("Button") + .a({ n: "press", v: this.client._event("NEST_MODEL") }) + .a({ n: "text", v: "Update only nested MODEL (no re-render)" }) + .tag("Input") + .a({ n: "value", v: this.client._bind("mv_input_main") }); + + this.client.view_display(lo_view.stringify()); + + } + + nest_view_display() { + + 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" }) + .ele("Page") + .a({ n: "title", v: "Nested View" }) + .tag("Button") + .a({ n: "press", v: this.client._event("TEST") }) + .a({ n: "text", v: "event" }) + .tag("Input") + .a({ n: "value", v: this.client._bind("mv_input_nest") }); + + this.client.nest_view_display({ val: lo_view_nested.stringify(), id: "test", method_insert: "addContent", method_destroy: "removeAllContent" }); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_067.js b/srv/apps/z2ui5_cl_smp_app_067.js new file mode 100644 index 0000000..c6ae480 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_067.js @@ -0,0 +1,154 @@ +// @keywords amount decimals leading zeros number format +// @summary Formats amounts with sap.ui.model.type.Currency, so decimals and leading zeros follow the currency rather than the ABAP field. +// @docs https://abap2ui5.github.io/docs/cookbook/model/formatter +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_067.clas.abap +import { defineApp, t, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_067", class { + + amount = t.packed(14, 3); + currency = ""; + numeric = t.numc(12); + + main(client) { + + this.client = client; + if (client.check_on_init()) { + + this.numeric = "000000000012"; + this.amount = "123456789.123"; + this.currency = "USD"; + this.view_display(); + + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event("BUTTON")) { + // the roundtrip IS the demo: the edited amounts travel back and re-render + this.view_display(); + } + + } + + view_display() { + + const page = 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:form", v: "sap.ui.layout.form" }) + .ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Binding - Currency Amounts (sap.ui.model.type.Currency)" }) + .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: "Formats amounts with the sap.ui.model.type.Currency type and its format options, " + + "and shows how to strip the leading zeros from a numeric field." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Currency" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }) + .tag("Title") + .a({ n: "text", v: "Input" }) + .tag("Label") + .a({ n: "text", v: "Documentation" }) + .tag("Link") + .a({ n: "text", v: "https://sdk.openui5.org/entity/sap.ui.model.type.Currency" }) + .a({ n: "href", v: "https://sdk.openui5.org/entity/sap.ui.model.type.Currency" }) + .a({ n: "target", v: "_blank" }) + .tag("Label") + .a({ n: "text", v: "One field" }) + .tag("Input") + .a({ n: "value", v: `{ parts: [ '${this.client._bind({ val: "amount", + path: true })}', '${this.client._bind({ + val: "currency", + path: true })}'], type: 'sap.ui.model.type.Currency' }` }) + .tag("Label") + .a({ n: "text", v: "Two field" }) + .tag("Input") + .a({ n: "value", v: `{ parts: [ '${this.client._bind({ val: "amount", + path: true })}', '${this.client._bind({ + val: "currency", + path: true })}'], type: 'sap.ui.model.type.Currency' , formatOptions: {showMeasure: false} }` }) + .tag("Input") + .a({ n: "value", v: `{ parts: [ '${this.client._bind({ val: "amount", + path: true })}', '${this.client._bind({ + val: "currency", + path: true })}'], type: 'sap.ui.model.type.Currency' , formatOptions: {showNumber: false} }` }) + .tag("Label") + .a({ n: "text", v: "Default" }) + .tag("Text") + .a({ n: "text", v: `{ parts: [ '${this.client._bind({ val: "amount", + path: true })}', '${this.client._bind({ + val: "currency", + path: true })}'], type: 'sap.ui.model.type.Currency' }` }) + .tag("Label") + .a({ n: "text", v: "preserveDecimals:false" }) + .tag("Text") + .a({ n: "text", v: `{ parts: [ '${this.client._bind({ val: "amount", + path: true })}', '` + this.client._bind({ + val: "currency", + path: true }) + + `'], type: 'sap.ui.model.type.Currency' , formatOptions: { preserveDecimals : false } }` }) + .tag("Label") + .a({ n: "text", v: "currencyCode:false" }) + .tag("Text") + .a({ n: "text", v: `{ parts: [ '${this.client._bind({ val: "amount", + path: true })}', '` + this.client._bind({ + val: "currency", + path: true }) + + `'], type: 'sap.ui.model.type.Currency' , formatOptions: { currencyCode : false } }` }) + .tag("Label") + .a({ n: "text", v: "style:'short'" }) + .tag("Text") + .a({ n: "text", v: `{ parts: [ '${this.client._bind({ val: "amount", + path: true })}', '${this.client._bind({ + val: "currency", + path: true })}'], type: 'sap.ui.model.type.Currency' , formatOptions: { style : 'short' } }` }) + .tag("Label") + .a({ n: "text", v: "style:'long'" }) + .tag("Text") + .a({ n: "text", v: `{ parts: [ '${this.client._bind({ val: "amount", + path: true })}', '${this.client._bind({ + val: "currency", + path: true })}'], type: 'sap.ui.model.type.Currency' , formatOptions: { style : 'long' } }` }) + .tag("Label") + .a({ n: "text", v: "event" }) + .tag("Button") + .a({ n: "press", v: this.client._event("BUTTON") }) + .a({ n: "text", v: "send" }); + + page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "No Zeros" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }) + .tag("Title") + .a({ n: "text", v: "Input" }) + .tag("Label") + .a({ n: "text", v: "Documentation" }) + .tag("Link") + .a({ n: "text", v: "https://sdk.openui5.org/api/sap.ui.model.odata.type.String%23methods/formatValue" }) + .a({ n: "href", v: "https://sdk.openui5.org/api/sap.ui.model.odata.type.String%23methods/formatValue" }) + .a({ n: "target", v: "_blank" }) + .tag("Label") + .a({ n: "text", v: "Numeric" }) + .tag("Input") + .a({ n: "value", v: this.client._bind({ val: "numeric" }) }) + .tag("Label") + .a({ n: "text", v: "Without leading Zeros" }) + .tag("Text") + .a({ n: "text", v: `{path : '${this.client._bind({ + val: "numeric", + path: true })}', type : 'sap.ui.model.odata.type.String', constraints : { isDigitSequence : true } }` }); + + this.client.view_display(page.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_088.js b/srv/apps/z2ui5_cl_smp_app_088.js new file mode 100644 index 0000000..8571f4b --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_088.js @@ -0,0 +1,82 @@ +// @keywords navcontainer icontabbar icontabheader page switch control_by_id whitelisted +// @summary Switches the page of a NavContainer and the tab of an IconTabBar by ID, so navigation inside a view costs no roundtrip. +// @docs https://abap2ui5.github.io/docs/cookbook/event_navigation/frontend +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_088.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_088", class { + + mv_selected_key = ""; + + main(client) { + + this.client = client; + // no event branch: the tab switch is answered on the client by the + // follow-up action below, nothing reaches the backend + if (client.check_on_navigated()) { + this.view_display(); + } + + } + + view_display() { + + const page = 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" }) + .ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Control Behaviour - Switch NavContainer Page by ID" }) + .a({ n: "showNavButton", b: this.client.check_app_prev_stack() }) + .a({ n: "navButtonPress", v: this.client._event_nav_app_leave() }) + .ele("content"); + + page.tag("MessageStrip") + .a({ n: "text", v: "Selecting a tab in the IconTabHeader switches the NavContainer page on the client via the " + + "generic CONTROL_BY_ID front-end action (whitelisted method 'to'), without a backend roundtrip." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele("IconTabHeader") + .a({ n: "selectedKey", v: this.client._bind("mv_selected_key") }) + .a({ n: "select", v: this.client.follow_up_action({ val: this.client.cs_event.control_by_id, + t_arg: [ "NavCon", "to", "${$parameters>/key}" ] }) }) + .a({ n: "mode", v: "Inline" }) + .ele("items") + .ele("IconTabFilter") + .a({ n: "text", v: "Home" }) + .a({ n: "key", v: "page1" }) + .end() + .ele("IconTabFilter") + .a({ n: "text", v: "Applications" }) + .a({ n: "key", v: "page2" }) + .end() + .ele("IconTabFilter") + .a({ n: "text", v: "Users and Groups" }) + .a({ n: "key", v: "page3" }); + + page.ele("NavContainer") + .a({ n: "initialPage", v: "page1" }) + .a({ n: "id", v: "NavCon" }) + .a({ n: "defaultTransitionName", v: "flip" }) + .ele("pages") + .ele("Page") + .a({ n: "title", v: "first page" }) + .a({ n: "id", v: "page1" }) + .end() + .ele("Page") + .a({ n: "title", v: "second page" }) + .a({ n: "id", v: "page2" }) + .end() + .ele("Page") + .a({ n: "title", v: "third page" }) + .a({ n: "id", v: "page3" }); + + this.client.view_display(page.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_109.js b/srv/apps/z2ui5_cl_smp_app_109.js new file mode 100644 index 0000000..6e5b955 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_109.js @@ -0,0 +1,147 @@ +// @keywords quickview contact card links grouped fields +// @summary A QuickView contact card in a Popover: grouped fields, and links that call the phone or the mail app. +// @docs https://abap2ui5.github.io/docs/cookbook/popup_popover/popover +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_109.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_109", class { + + mv_placement = ""; + + popover_display(id) { + + const view = 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" }); + view.ele("QuickView") + .a({ n: "placement", t: this.mv_placement }) + .ele("QuickViewPage") + .a({ n: "description", v: "Enjoy" }) + .a({ n: "header", v: "Employee Info" }) + .a({ n: "pageId", v: "employeePageId" }) + .a({ n: "title", v: "choper725" }) + .a({ n: "titleUrl", v: "https://github.com/abap2UI5/abap2UI5" }) + .ele("QuickViewGroup") + .a({ n: "heading", v: "Contact Details" }) + .ele("QuickViewGroupElement") + .a({ n: "label", v: "Mobile" }) + .a({ n: "type", v: "mobile" }) + .a({ n: "value", v: "123-456-789" }) + .end() + .ele("QuickViewGroupElement") + .a({ n: "label", v: "Phone" }) + .a({ n: "type", v: "phone" }) + .a({ n: "value", v: "789-456-123" }) + .end() + .ele("QuickViewGroupElement") + .a({ n: "emailSubject", v: "Subject" }) + .a({ n: "label", v: "Email" }) + .a({ n: "type", v: "email" }) + .a({ n: "value", v: "thisisemail@email.com" }) + .end() + .end() + .ele("QuickViewGroup") + .a({ n: "heading", v: "Company" }) + .ele("QuickViewGroupElement") + .a({ n: "label", v: "Name" }) + .a({ n: "type", v: "link" }) + .a({ n: "url", v: "https://github.com/abap2UI5/abap2UI5" }) + .a({ n: "value", v: "Adventure Company" }) + .end() + .ele("QuickViewGroupElement") + .a({ n: "label", v: "Address" }) + .a({ n: "value", v: "Here" }) + .end(); + + this.client.popover_display({ xml: view.stringify(), by_id: id }); + + } + + 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:form", v: "sap.ui.layout.form" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Popover - QuickView Contact Card" }) + .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: "Opens a QuickView popover, a compact contact card with grouped fields and links, " + + "anchored to a button; the segmented button sets its placement." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "QuickView Popover" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }) + .tag("Title") + .a({ n: "text", v: "QuickView Popover" }) + .tag("Label") + .a({ n: "text", v: "placement" }) + .ele("SegmentedButton") + .a({ n: "selectedKey", v: this.client._bind("mv_placement") }) + .ele("items") + .tag("SegmentedButtonItem") + .a({ n: "icon", v: "sap-icon://add-favorite" }) + .a({ n: "key", v: "Left" }) + .a({ n: "text", v: "Left" }) + .tag("SegmentedButtonItem") + .a({ n: "icon", v: "sap-icon://accept" }) + .a({ n: "key", v: "Top" }) + .a({ n: "text", v: "Top" }) + .tag("SegmentedButtonItem") + .a({ n: "icon", v: "sap-icon://accept" }) + .a({ n: "key", v: "Bottom" }) + .a({ n: "text", v: "Bottom" }) + .tag("SegmentedButtonItem") + .a({ n: "icon", v: "sap-icon://attachment" }) + .a({ n: "key", v: "Right" }) + .a({ n: "text", v: "Right" }) + .end() + .end() + .tag("Label") + .a({ n: "text", v: "popover" }) + .tag("Button") + .a({ n: "press", v: this.client._event("POPOVER") }) + .a({ n: "text", v: "show" }) + .a({ n: "id", v: "TEST" }) + .a({ n: "width", v: "10rem" }); + + this.client.view_display(view.stringify()); + + } + + main(client) { + + this.client = client; + if (client.check_on_init()) { + + this.mv_placement = "Left"; + this.view_display(); + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + on_event() { + + if (this.client.check_on_event("POPOVER")) { + this.popover_display("TEST"); + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_120.js b/srv/apps/z2ui5_cl_smp_app_120.js new file mode 100644 index 0000000..25f3f5f --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_120.js @@ -0,0 +1,106 @@ +// @keywords gps position latitude longitude altitude location +// @summary Asks the browser for the device's position - latitude, longitude and altitude - and what happens when the user says no. +// @docs https://abap2ui5.github.io/docs/cookbook/device_capabilities/geolocation +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_120.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_120", class { + + longitude = ""; + latitude = ""; + altitude = ""; + speed = ""; + altitudeaccuracy = ""; + accuracy = ""; + + main(client) { + + this.client = client; + if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event("GEOLOCATION_ERROR")) { + // the Geolocation control fires `error` when the position cannot be + // read; the code (1 = permission denied, 2 = position unavailable, + // 3 = timeout) and message are passed as event arguments. + client.message_box_display({ + text: `Location unavailable (${client.get_event_arg(1)}): ${client.get_event_arg(2)}`, + type: "error" }); + } + + } + + 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:form", v: "sap.ui.layout.form" }) + .a({ n: "xmlns:z2ui5", v: "z2ui5.cc" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Device - Geolocation from the Browser" }) + .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: "The geolocation custom control reads the device position from the browser and binds " + + "longitude, latitude, altitude, accuracy and speed into the read-only form below." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.tag({ n: "Geolocation", ns: "z2ui5" }) + // the position arrives with the roundtrip and the view re-renders with it + .a({ n: "finished", v: this.client._event("GEOLOCATION_LOADED") }) + .a({ n: "error", v: this.client._event({ val: "GEOLOCATION_ERROR", + t_arg: [ "${$parameters>/code}", + "${$parameters>/message}" ] }) }) + .a({ n: "longitude", v: this.client._bind("longitude") }) + .a({ n: "latitude", v: this.client._bind("latitude") }) + .a({ n: "altitude", v: this.client._bind("altitude") }) + .a({ n: "accuracy", v: this.client._bind("accuracy") }) + .a({ n: "altitudeAccuracy", v: this.client._bind("altitudeaccuracy") }) + .a({ n: "speed", v: this.client._bind("speed") }) + .ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Geolocation" }) + .a({ n: "editable", b: false }) + .ele({ n: "content", ns: "form" }) + .tag("Label") + .a({ n: "text", v: "Longitude" }) + .tag("Input") + .a({ n: "editable", b: false }) + .a({ n: "value", v: this.client._bind("longitude") }) + .tag("Label") + .a({ n: "text", v: "Latitude" }) + .tag("Input") + .a({ n: "editable", b: false }) + .a({ n: "value", v: this.client._bind("latitude") }) + .tag("Label") + .a({ n: "text", v: "Altitude" }) + .tag("Input") + .a({ n: "editable", b: false }) + .a({ n: "value", v: this.client._bind("altitude") }) + .tag("Label") + .a({ n: "text", v: "Accuracy" }) + .tag("Input") + .a({ n: "editable", b: false }) + .a({ n: "value", v: this.client._bind("accuracy") }) + .tag("Label") + .a({ n: "text", v: "AltitudeAccuracy" }) + .tag("Input") + .a({ n: "editable", b: false }) + .a({ n: "value", v: this.client._bind("altitudeaccuracy") }) + .tag("Label") + .a({ n: "text", v: "Speed" }) + .tag("Input") + .a({ n: "editable", b: false }) + .a({ n: "value", v: this.client._bind("speed") }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_122.js b/srv/apps/z2ui5_cl_smp_app_122.js new file mode 100644 index 0000000..7239eb9 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_122.js @@ -0,0 +1,231 @@ +// @keywords client info ui5 version theme os user agent device cs_device browser orientation constants +// @summary Asks the frontend what it is: UI5 version, theme, operating system, browser and user agent, in one call. +// @docs https://abap2ui5.github.io/docs/cookbook/device_capabilities/info +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_122.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_122", class { + + ui5_version = ""; + ui5_theme = ""; + ui5_gav = ""; + ui5_build_timestamp = ""; + device_systemtype = ""; + device_os = ""; + device_os_version = ""; + device_browser = ""; + device_browser_version = ""; + device_orientation = ""; + device_phone = false; + device_desktop = false; + device_tablet = false; + device_combi = false; + device_touch = false; + device_pointer = false; + device_retina = false; + device_height = ""; + device_width = ""; + browser_label = ""; + os_label = ""; + orientation_label = ""; + + read_frontend_info() { + + const ls_get = this.client.get(); + + this.device_browser = ls_get.s_device.browser.name; + this.device_browser_version = ls_get.s_device.browser.version; + this.device_os = ls_get.s_device.os.name; + this.device_os_version = ls_get.s_device.os.version; + this.device_systemtype = ls_get.s_device.system; + this.device_orientation = ls_get.s_device.orientation; + this.device_height = String(ls_get.s_device.resize.height); + this.device_width = String(ls_get.s_device.resize.width); + this.device_phone = (ls_get.s_device.system === z2ui5_if_client.cs_device.system.phone); + this.device_desktop = (ls_get.s_device.system === z2ui5_if_client.cs_device.system.desktop); + this.device_tablet = (ls_get.s_device.system === z2ui5_if_client.cs_device.system.tablet); + this.device_combi = (ls_get.s_device.system === z2ui5_if_client.cs_device.system.combi); + this.device_touch = ls_get.s_device.support.touch; + this.device_pointer = ls_get.s_device.support.pointer; + this.device_retina = ls_get.s_device.support.retina; + this.ui5_version = ls_get.s_ui5.version; + this.ui5_theme = ls_get.s_ui5.theme; + this.ui5_gav = ls_get.s_ui5.gav; + this.ui5_build_timestamp = ls_get.s_ui5.build_timestamp; + + // the raw values are short codes (cr, ff, win, mac, ...). cs_device + // names every one of them, so an app branches on the constant rather + // than on a string it would have to know - here into a label per group + this.browser_label = (ls_get.s_device.browser.name === z2ui5_if_client.cs_device.browser.chrome ? "Google Chrome (or Chromium)" + : ls_get.s_device.browser.name === z2ui5_if_client.cs_device.browser.firefox ? "Mozilla Firefox" + : ls_get.s_device.browser.name === z2ui5_if_client.cs_device.browser.safari ? "Apple Safari" + : ls_get.s_device.browser.name === z2ui5_if_client.cs_device.browser.edge ? "Microsoft Edge" + : "not one cs_device-browser names"); + this.os_label = (ls_get.s_device.os.name === z2ui5_if_client.cs_device.os.windows ? "Windows" + : ls_get.s_device.os.name === z2ui5_if_client.cs_device.os.macintosh ? "macOS" + : ls_get.s_device.os.name === z2ui5_if_client.cs_device.os.linux ? "Linux" + : ls_get.s_device.os.name === z2ui5_if_client.cs_device.os.ios ? "iOS" + : ls_get.s_device.os.name === z2ui5_if_client.cs_device.os.android ? "Android" + : "not one cs_device-os names"); + this.orientation_label = (ls_get.s_device.orientation === z2ui5_if_client.cs_device.orientation.portrait ? "portrait - one column would fit best" + : ls_get.s_device.orientation === z2ui5_if_client.cs_device.orientation.landscape ? "landscape - room for two columns" + : "not one cs_device-orientation names"); + + } + + 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:form", v: "sap.ui.layout.form" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Device - Frontend Info: UI5 Version, Theme, OS, Browser" }) + .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: "Reads frontend information from the client - UI5 version and theme plus device, " + + "OS and browser details - and shows each value in a read-only form. The three labels " + + "are decided with the cs_device constants, the way an app would branch on them." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Information" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }) + .tag("Label") + .a({ n: "text", v: "device_browser" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("device_browser") }) + .tag("Label") + .a({ n: "text", v: "device_browser_version" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("device_browser_version") }) + .tag("Label") + .a({ n: "text", v: "device_os" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("device_os") }) + .tag("Label") + .a({ n: "text", v: "device_os_version" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("device_os_version") }) + .tag("Label") + .a({ n: "text", v: "device_systemtype" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("device_systemtype") }) + .tag("Label") + .a({ n: "text", v: "device_orientation" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("device_orientation") }) + .tag("Label") + .a({ n: "text", v: "browser, by cs_device-browser" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("browser_label") }) + .tag("Label") + .a({ n: "text", v: "os, by cs_device-os" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("os_label") }) + .tag("Label") + .a({ n: "text", v: "orientation, by cs_device-orientation" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("orientation_label") }) + .tag("Label") + .a({ n: "text", v: "device_height" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("device_height") }) + .tag("Label") + .a({ n: "text", v: "device_width" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("device_width") }) + .tag("Label") + .a({ n: "text", v: "device_phone" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("device_phone") }) + .tag("Label") + .a({ n: "text", v: "device_desktop" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("device_desktop") }) + .tag("Label") + .a({ n: "text", v: "device_tablet" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("device_tablet") }) + .tag("Label") + .a({ n: "text", v: "device_combi" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("device_combi") }) + .tag("Label") + .a({ n: "text", v: "device_touch" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("device_touch") }) + .tag("Label") + .a({ n: "text", v: "device_pointer" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("device_pointer") }) + .tag("Label") + .a({ n: "text", v: "device_retina" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("device_retina") }) + .tag("Label") + .a({ n: "text", v: "ui5_version" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("ui5_version") }) + .tag("Label") + .a({ n: "text", v: "ui5_theme" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("ui5_theme") }) + .tag("Label") + .a({ n: "text", v: "ui5_gav" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("ui5_gav") }) + .tag("Label") + .a({ n: "text", v: "ui5_build_timestamp" }) + .tag("Input") + .a({ n: "enabled", b: false }) + .a({ n: "value", v: this.client._bind("ui5_build_timestamp") }); + this.client.view_display(view.stringify()); + + } + + main(client) { + + this.client = client; + if (client.check_on_init()) { + + this.read_frontend_info(); + this.view_display(); + } else if (client.check_on_navigated()) { + this.view_display(); + + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_125.js b/srv/apps/z2ui5_cl_smp_app_125.js index 905b295..b1008f5 100644 --- a/srv/apps/z2ui5_cl_smp_app_125.js +++ b/srv/apps/z2ui5_cl_smp_app_125.js @@ -21,13 +21,13 @@ defineApp("Z2UI5_CL_SMP_APP_125", class { .a({ n: "xmlns:form", v: "sap.ui.layout.form" }); const page = view.ele("Shell") .ele("Page") - .a({ n: "title", v: "cap2UI5 - Browser - Set the Tab Title" }) + .a({ n: "title", v: "abap2UI5 - Browser - Set the Tab Title" }) .a({ n: "showNavButton", b: client.check_app_prev_stack() }) .a({ n: "navButtonPress", v: client._event_nav_app_leave() }); page.tag("MessageStrip") .a({ n: "text", v: "Enter a title and press the button to run the set_title front-end action, which updates " + - "the browser tab title (document.title) without reloading the page." }) + "the browser tab title (document.title) without reloading the page." }) .a({ n: "type", v: "Information" }) .a({ n: "showIcon", b: true }) .a({ n: "class", v: "sapUiSmallMargin" }); @@ -49,7 +49,7 @@ defineApp("Z2UI5_CL_SMP_APP_125", class { client.follow_up_action({ val: z2ui5_if_client.cs_event.set_title, - t_arg: [this.title] }); + t_arg: [ this.title ] }); } diff --git a/srv/apps/z2ui5_cl_smp_app_133.js b/srv/apps/z2ui5_cl_smp_app_133.js new file mode 100644 index 0000000..6f88eda --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_133.js @@ -0,0 +1,94 @@ +// @keywords cursor set_focus selection position textfield +// @summary Sets the focus into an Input and selects its text, so the next keystroke overwrites rather than appends. +// @docs https://abap2ui5.github.io/docs/cookbook/browser_interaction/focus +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_133.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_133", class { + + field_01 = ""; + field_02 = ""; + selstart = ""; + selend = ""; + + 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:form", v: "sap.ui.layout.form" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Focus - Set Focus and Select Text in an Input" }) + .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: "Pressing a button runs the set_focus front-end action, which moves keyboard focus to the " + + "target input and selects the text between the given start and end positions." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Focus & Cursor" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }) + .tag("Title") + .a({ n: "text", v: "Input" }) + .tag("Label") + .a({ n: "text", v: "Sel_Start" }) + .tag("Input") + .a({ n: "value", v: this.client._bind("selstart") }) + .tag("Label") + .a({ n: "text", v: "Sel_End" }) + .tag("Input") + .a({ n: "value", v: this.client._bind("selend") }) + .tag("Label") + .a({ n: "text", v: "field_01" }) + .tag("Input") + .a({ n: "id", v: "BUTTON01" }) + .a({ n: "value", v: this.client._bind("field_01") }) + .tag("Button") + .a({ n: "press", v: this.client._event("BUTTON01") }) + .a({ n: "text", v: "focus here" }) + .tag("Label") + .a({ n: "text", v: "field_02" }) + .tag("Input") + .a({ n: "id", v: "BUTTON02" }) + .a({ n: "value", v: this.client._bind("field_02") }) + .tag("Button") + .a({ n: "press", v: this.client._event("BUTTON02") }) + .a({ n: "text", v: "focus here" }); + + this.client.view_display(view.stringify()); + + } + + main(client) { + + this.client = client; + if (client.check_on_init()) { + + this.field_01 = "this is a text"; + this.field_02 = "this is another text"; + this.selstart = "3"; + this.selend = "7"; + this.view_display(); + + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event("BUTTON01") || client.check_on_event("BUTTON02")) { + + client.follow_up_action({ + val: z2ui5_if_client.cs_event.set_focus, + t_arg: [ client.get_event(), this.selstart, this.selend ] }); + client.message_toast_display(`focus changed`); + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_143.js b/srv/apps/z2ui5_cl_smp_app_143.js new file mode 100644 index 0000000..a56713f --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_143.js @@ -0,0 +1,152 @@ +// @keywords column filter reset refresh uitableext grid alv +// @summary Keeps the active sap.ui.table column filters across a view model update, through the abap2UI5 uitableext custom control - without it they are reset. +// @docs https://abap2ui5.github.io/docs/cookbook/model/tables +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_143.clas.abap +import { defineApp, t, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +const ty_s_data = { + field1: "", + field2: "", + field3: "", +}; + +defineApp("Z2UI5_CL_SMP_APP_143", class { + + gt_data = t.table(ty_s_data); + + main(client) { + + this.client = client; + if (client.check_on_init()) { + this.on_init(); + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + on_init() { + + this.gt_data = [ + { field1: "21", field2: "T1", field3: "TEXT1" }, + { field1: "22", field2: "T1", field3: "TEXT1" }, + { field1: "23", field2: "T2", field3: "TEXT1" }, + { field1: "24", field2: "T2", field3: "TEXT2" }, + { field1: "25", field2: "T3", field3: "TEXT2" } ]; + + this.view_display(); + + } + + on_event() { + + // the roundtrip re-renders the view on purpose: that is the model update + // the column filters have to survive + if (this.client.check_on_event("ROW_ACTION_ITEM_ADD")) { + this.client.message_toast_display(`row ${this.client.get_event_arg()} pressed`); + this.view_display(); + } + + } + + 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:f", v: "sap.f" }) + .a({ n: "xmlns:table", v: "sap.ui.table" }) + .a({ n: "xmlns:z2ui5", v: "z2ui5.cc" }); + + const page1 = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Grid Table - Keep Column Filters on Refresh" }) + .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" }); + + page1.tag("MessageStrip") + .a({ n: "text", v: "This sample uses the abap2UI5 uitableext custom control so the active sap.ui.table column " + + "filters are preserved across a view model update instead of being reset." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + const page = page1.ele({ n: "DynamicPage", ns: "f" }) + .a({ n: "headerExpanded", b: true }); + page1.tag({ n: "UITableExt", ns: "z2ui5" }) + .a({ n: "tableId", v: "Table1" }); + + const header_title = page.ele({ n: "title", ns: "f" }) + .ele({ n: "DynamicPageTitle", ns: "f" }); + header_title.ele({ n: "heading", ns: "f" }) + .ele("HBox") + .tag("Title") + .a({ n: "text", v: "Table" }); + header_title.ele({ n: "expandedContent", ns: "f" }); + header_title.ele({ n: "snappedContent", ns: "f" }); + + const cont = page.ele({ n: "content", ns: "f" }); + + const table = cont.ele("VBox") + .ele({ n: "Table", ns: "table" }) + .a({ n: "rows", v: this.client._bind("gt_data") }) + .a({ n: "alternateRowColors", b: true }) + .a({ n: "enableCellFilter", b: true }) + .a({ n: "fixedColumnCount", v: "1" }) + .a({ n: "rowActionCount", v: "1" }) + .a({ n: "selectionMode", v: "None" }) + .a({ n: "id", v: "Table1" }); + + table.ele({ n: "columns", ns: "table" }) + .ele({ n: "Column", ns: "table" }) + .a({ n: "sortProperty", v: "FIELD1" }) + .a({ n: "autoResizable", v: "true" }) + .a({ n: "filterProperty", v: "FIELD1" }) + .tag("Text") + .a({ n: "text", v: "Field1" }) + .ele({ n: "template", ns: "table" }) + .tag("Text") + .a({ n: "text", v: "{FIELD1}" }) + .end() + .end() + .ele({ n: "Column", ns: "table" }) + .a({ n: "sortProperty", v: "FIELD2" }) + .a({ n: "autoResizable", v: "true" }) + .a({ n: "filterProperty", v: "FIELD2" }) + .tag("Text") + .a({ n: "text", v: "Field2" }) + .ele({ n: "template", ns: "table" }) + .tag("Text") + .a({ n: "text", v: "{FIELD2}" }) + .end() + .end() + .ele({ n: "Column", ns: "table" }) + .a({ n: "sortProperty", v: "FIELD3" }) + .a({ n: "autoResizable", v: "true" }) + .a({ n: "filterProperty", v: "FIELD3" }) + .tag("Text") + .a({ n: "text", v: "Field3" }) + .ele({ n: "template", ns: "table" }) + .tag("Text") + .a({ n: "text", v: "{FIELD3}" }) + .end() + .end() + .end() + .ele({ n: "rowActionTemplate", ns: "table" }) + .ele({ n: "RowAction", ns: "table" }) + .ele({ n: "RowActionItem", ns: "table" }) + .a({ n: "icon", v: "sap-icon://add" }) + .a({ n: "text", v: "Add" }) + .a({ n: "press", v: this.client._event({ val: "ROW_ACTION_ITEM_ADD", arg: "${FIELD1}" }) }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_161.js b/srv/apps/z2ui5_cl_smp_app_161.js index 865f9ee..943f6af 100644 --- a/srv/apps/z2ui5_cl_smp_app_161.js +++ b/srv/apps/z2ui5_cl_smp_app_161.js @@ -67,13 +67,13 @@ defineApp("Z2UI5_CL_SMP_APP_161", class { .a({ n: "xmlns:mvc", v: "sap.ui.core.mvc" }); const page = view.ele("Shell") .ele("Page") - .a({ n: "title", v: "cap2UI5 - Popup - Dialog inside a Dialog" }) + .a({ n: "title", v: "abap2UI5 - Popup - Dialog inside a Dialog" }) .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: "This sample opens a popup from a button and then chains to a second popup " + - "from within the first one." }) + "from within the first one." }) .a({ n: "type", v: "Information" }) .a({ n: "showIcon", b: true }) .a({ n: "class", v: "sapUiSmallMargin" }); @@ -119,5 +119,4 @@ defineApp("Z2UI5_CL_SMP_APP_161", class { this.on_event(); } - }); diff --git a/srv/apps/z2ui5_cl_smp_app_163.js b/srv/apps/z2ui5_cl_smp_app_163.js new file mode 100644 index 0000000..949a761 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_163.js @@ -0,0 +1,104 @@ +// @keywords menu menuitem popover button messagetoast require module +// @summary A Menu opened as a popover from a Button, whose items call a UI5 module loaded with core:require, so the click is answered in the frontend. +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_163.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_163", class { + + on_event() { + + if (this.client.check_on_event("OPEN_MENU")) { + this.view_menu(); + } + + } + + view_menu() { + + const menu_view = 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" }); + + menu_view.a({ n: "core:require", v: "{ MessageToast: 'sap/m/MessageToast' }" }); + + // Every MenuItem below calls the core:require'd MessageToast in the + // BROWSER through an expression binding - no roundtrip, which is the point + // of this sample. binding-for-event is about a MODEL binding landing on an + // event by accident; this is the deliberate other thing. + menu_view.ele("Menu") + .a({ n: "title", v: "Choose Your Action" }) + .tag("MenuItem") + .a({ n: "press", v: "MessageToast.show('selected action is ' + ${$source>/text})" }) + .a({ n: "text", v: "Accept" }) + .a({ n: "icon", v: "sap-icon://accept" }) + .tag("MenuItem") + .a({ n: "press", v: "MessageToast.show('selected action is ' + ${$source>/text})" }) + .a({ n: "text", v: "Reject" }) + .a({ n: "icon", v: "sap-icon://decline" }) + .tag("MenuItem") + .a({ n: "press", v: "MessageToast.show('selected action is ' + ${$source>/text})" }) + .a({ n: "text", v: "Email" }) + .a({ n: "icon", v: "sap-icon://email" }) + .tag("MenuItem") + .a({ n: "press", v: "MessageToast.show('selected action is ' + ${$source>/text})" }) + .a({ n: "text", v: "Forward" }) + .a({ n: "icon", v: "sap-icon://forward" }) + .tag("MenuItem") + .a({ n: "press", v: "MessageToast.show('selected action is ' + ${$source>/text})" }) + .a({ n: "text", v: "Delete" }) + .a({ n: "icon", v: "sap-icon://delete" }) + .tag("MenuItem") + .a({ n: "press", v: "MessageToast.show('selected action is ' + ${$source>/text})" }) + .a({ n: "text", v: "Other" }); + + this.client.popover_display({ xml: menu_view.stringify(), by_id: "menuButton" }); + + } + + view_display() { + + let 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" }); + + view = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Menu - Menu as Popover with core:require" }) + .a({ n: "showNavButton", b: this.client.check_app_prev_stack() }) + .a({ n: "navButtonPress", v: this.client._event_nav_app_leave() }) + .a({ n: "id", v: "page_main" }); + + view.tag("MessageStrip") + .a({ n: "text", v: "This sample opens a Menu as a popover anchored to a button; choosing an " + + "item shows the selected action in a MessageToast." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + const vbox = view.ele("VBox"); + + vbox.tag("Button") + .a({ n: "press", v: this.client._event("OPEN_MENU") }) + .a({ n: "text", v: "Open Menu" }) + .a({ n: "id", v: "menuButton" }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + this.client.view_display(view.stringify()); + + } + + main(client) { + + this.client = client; + if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_166.js b/srv/apps/z2ui5_cl_smp_app_166.js new file mode 100644 index 0000000..71b9349 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_166.js @@ -0,0 +1,97 @@ +// @keywords structure component include flat form level +// @summary Binds a form to a structure with INCLUDEs, so the included components are reachable under their own names on one flat level. +// @docs https://abap2ui5.github.io/docs/cookbook/model/binding +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_166.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +const ty_s_struc_incl = { + incl_title: "", + incl_value: "", + incl_value2: "", +}; + +const ty_s_struc = { + title: "", + value: "", + value2: "", +}; + +defineApp("Z2UI5_CL_SMP_APP_166", class { + + ms_struc = ty_s_struc; + + ms_struc2 = { + ...ty_s_struc, + ...ty_s_struc_incl, + }; + + 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" }); + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Binding - Structure Fields and INCLUDEs" }) + .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: "This sample demonstrates structure-level binding: each input is bound to a " + + "field of a flat structure, including fields pulled in via INCLUDE." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.tag("Input") + .a({ n: "value", v: this.client._bind({ val: "ms_struc-title" }) }); + page.tag("Input") + .a({ n: "value", v: this.client._bind({ val: "ms_struc-value" }) }); + page.tag("Input") + .a({ n: "value", v: this.client._bind({ val: "ms_struc-value2" }) }); + + page.tag("Input") + .a({ n: "value", v: this.client._bind({ val: "ms_struc2-title" }) }); + page.tag("Input") + .a({ n: "value", v: this.client._bind({ val: "ms_struc2-value" }) }); + page.tag("Input") + .a({ n: "value", v: this.client._bind({ val: "ms_struc2-value2" }) }); + + page.tag("Input") + .a({ n: "value", v: this.client._bind({ val: "ms_struc2-incl_title" }) }); + page.tag("Input") + .a({ n: "value", v: this.client._bind({ val: "ms_struc2-incl_value" }) }); + page.tag("Input") + .a({ n: "value", v: this.client._bind({ val: "ms_struc2-incl_value2" }) }); + + this.client.view_display(view.stringify()); + + } + + main(client) { + + this.client = client; + + if (client.check_on_init()) { + + this.ms_struc = { ...this.ms_struc, title: "title" }; + this.ms_struc = { ...this.ms_struc, value: "val01" }; + this.ms_struc = { ...this.ms_struc, value2: "val02" }; + + this.ms_struc2 = { ...this.ms_struc2, title: "title" }; + this.ms_struc2 = { ...this.ms_struc2, value: "val01" }; + this.ms_struc2 = { ...this.ms_struc2, value2: "val02" }; + this.ms_struc2 = { ...this.ms_struc2, incl_title: "title_incl" }; + this.ms_struc2 = { ...this.ms_struc2, incl_value: "val01_incl" }; + this.ms_struc2 = { ...this.ms_struc2, incl_value2: "val02_incl" }; + + this.view_display(); + } else if (client.check_on_navigated()) { + this.view_display(); + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_167.js b/srv/apps/z2ui5_cl_smp_app_167.js index 531e477..b0f2dc6 100644 --- a/srv/apps/z2ui5_cl_smp_app_167.js +++ b/srv/apps/z2ui5_cl_smp_app_167.js @@ -18,13 +18,13 @@ defineApp("Z2UI5_CL_SMP_APP_167", class { .a({ n: "xmlns:mvc", v: "sap.ui.core.mvc" }); const page = view.ele("Shell") .ele("Page") - .a({ n: "title", v: "cap2UI5 - Event - Extra Arguments with t_arg" }) + .a({ n: "title", v: "abap2UI5 - Event - Extra Arguments with t_arg" }) .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: "This sample shows how to pass extra arguments to an event via t_arg - fixed " + - "values, model values, or client-side expressions - and read them in the backend." }) + "values, model values, or client-side expressions - and read them in the backend." }) .a({ n: "type", v: "Information" }) .a({ n: "showIcon", b: true }) .a({ n: "class", v: "sapUiSmallMargin" }); @@ -74,6 +74,7 @@ defineApp("Z2UI5_CL_SMP_APP_167", class { switch (client.get_event()) { case "EVENT_FIX_VAL": case "EVENT_MODEL_VALUE": case "SOURCE_PROPERTY_TEXT": case "EVENT_PROPERTY_VALUE": case "PARENT_PROPERTY_ID": client.message_box_display(`backend event: ${client.get_event_arg()}`); + break; } } diff --git a/srv/apps/z2ui5_cl_smp_app_170.js b/srv/apps/z2ui5_cl_smp_app_170.js new file mode 100644 index 0000000..c336426 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_170.js @@ -0,0 +1,168 @@ +// @keywords navcontainer dialog pages back forward +// @summary A NavContainer inside a dialog: several pages in one popup, with back and forward between them. +// @docs https://abap2ui5.github.io/docs/cookbook/popup_popover/popup +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_170.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_170", class { + + mv_selected_key = ""; + + simple_popup1() { + + const 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" }); + + const dialog = popup.ele("Dialog") + .a({ n: "stretch", b: true }) + .a({ n: "afterClose", v: this.client._event("BTN_OK_1ND") }) + .ele("content"); + + dialog.ele("IconTabBar") + .a({ n: "select", v: this.client.follow_up_action({ val: this.client.cs_event.control_by_id, + view: this.client.cs_view.popup, + t_arg: [ "NavCon", "to", "${$parameters>/selectedKey}" ] }) }) + .a({ n: "expandable", b: false }) + .a({ n: "expanded", b: true }) + .a({ n: "headerMode", v: "Inline" }) + .a({ n: "selectedKey", v: this.client._bind("mv_selected_key") }) + .ele("items") + .ele("IconTabFilter") + .a({ n: "text", v: "Home" }) + .a({ n: "key", v: "page1" }) + .end() + .ele("IconTabFilter") + .a({ n: "text", v: "Applications" }) + .a({ n: "key", v: "page2" }) + .end() + .ele("IconTabFilter") + .a({ n: "text", v: "Users and Groups" }) + .a({ n: "key", v: "page3" }) + .end() + .end() + .ele("content") + .ele("VBox") + .a({ n: "height", v: "100%" }) + .ele("NavContainer") + .a({ n: "initialPage", v: "page1" }) + .a({ n: "id", v: "NavCon" }) + .a({ n: "height", v: "400px" }) + .a({ n: "defaultTransitionName", v: "flip" }) + .ele("pages") + .ele("Page") + .a({ n: "title", v: "first page" }) + .a({ n: "id", v: "page1" }) + .tag("Button") + .a({ n: "press", v: this.client._event("GOTO_2ND") }) + .a({ n: "text", v: "Open 2nd popup" }) + .end() + .ele("Page") + .a({ n: "title", v: "second page" }) + .a({ n: "id", v: "page2" }) + .end() + .ele("Page") + .a({ n: "title", v: "third page" }) + .a({ n: "id", v: "page3" }); + + dialog.end() + .ele("buttons") + .tag("Button") + .a({ n: "press", v: this.client._event("BTN_OK_1ND") }) + .a({ n: "text", v: "OK" }) + .a({ n: "type", v: "Emphasized" }); + + this.client.popup_display(popup.stringify()); + + } + + simple_popup2() { + + const 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" }); + + const dialog = popup.ele("Dialog") + .a({ n: "afterClose", v: this.client._event("BTN_OK_2ND") }) + .ele("content"); + + dialog.tag("Label") + .a({ n: "text", v: "this is a second popup" }); + + dialog.end() + .ele("buttons") + .tag("Button") + .a({ n: "press", v: this.client._event("BTN_OK_2ND") }) + .a({ n: "text", v: "GOTO 1ST POPUP" }) + .a({ n: "type", v: "Emphasized" }); + + this.client.popup_display(popup.stringify()); + + } + + 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" }); + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Popup - Navigate between Dialogs (NavContainer)" }) + .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: "Press the button to open a dialog; from there a second popup can be opened and navigated " + + "back to the first, demonstrating popup-to-popup navigation." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.tag("Button") + .a({ n: "press", v: this.client._event("POPUP") }) + .a({ n: "text", v: "Open Popup..." }); + + this.client.view_display(view.stringify()); + + } + + on_event() { + + switch (this.client.get_event()) { + case "GOTO_2ND": + this.simple_popup2(); + break; + + case "BTN_OK_2ND": + this.client.popup_destroy(); + this.simple_popup1(); + break; + + case "BTN_OK_1ND": + this.client.popup_destroy(); + break; + + case "POPUP": + this.simple_popup1(); + break; + } + + } + + main(client) { + + this.client = client; + if (client.check_on_navigated()) { + this.view_display(); + + } else if (client.check_on_event()) { + this.on_event(); + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_173.js b/srv/apps/z2ui5_cl_smp_app_173.js new file mode 100644 index 0000000..e494198 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_173.js @@ -0,0 +1,125 @@ +// @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. +// @docs https://abap2ui5.github.io/docs/cookbook/view/xml_templating +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_173.clas.abap +import { defineApp, t, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +const ty_s_data = { + name: "", + date: "", + age: "", +}; + +const ty_s_layout = { + fname: "", + merge: "", + visible: "", +}; + +defineApp("Z2UI5_CL_SMP_APP_173", class { + + mv_flag = false; + mt_layout = t.table(ty_s_layout); + mt_data = t.table(ty_s_data); + + 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 + const layout_path = `{template>${this.client._bind({ val: "mt_layout", path: true })}}`; + const flag_path = `{template>${this.client._bind({ val: "mv_flag", path: true })}}`; + + let 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" }) + .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: "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: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + 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: "L0" }) + .ele("Column") + .a({ n: "mergeDuplicates", v: "{L0>MERGE}" }) + .a({ n: "visible", v: "{L0>VISIBLE}" }) + .tag("Text") + .a({ n: "text", v: "{L0>FNAME}" }) + .end() + .end() + .end() + .ele("items") + .ele("ColumnListItem") + .ele("cells") + .ele({ n: "repeat", ns: "template" }) + .a({ n: "list", v: layout_path }) + .a({ n: "var", v: "L1" }) + .ele("ObjectIdentifier") + .a({ n: "text", v: "{= '{' + ${L1>FNAME} + '}' }" }); + + view.tag("Label") + .a({ n: "text", v: "IF Template (with re-rendering)" }); + view.tag("Switch") + .a({ n: "state", v: this.client._bind("mv_flag") }) + .a({ n: "change", v: this.client._event("CHANGE_FLAG") }); + view = view.ele("VBox"); + + view.ele({ n: "if", ns: "template" }) + .a({ n: "test", v: flag_path }) + .ele({ n: "then", ns: "template" }) + .tag({ n: "Icon", ns: "core" }) + .a({ n: "color", v: "green" }) + .a({ n: "src", v: "sap-icon://accept" }) + .end() + .ele({ n: "else", ns: "template" }) + .tag({ n: "Icon", ns: "core" }) + .a({ n: "color", v: "red" }) + .a({ n: "src", v: "sap-icon://decline" }); + + this.client.view_display(view.stringify()); + + } + + main(client) { + + this.client = client; + + if (client.check_on_init()) { + + this.mt_data = [ { name: "Theo", date: "01.01.2000", age: "5" }, + { name: "Lore", date: "01.01.2000", age: "1" } ]; + + this.mt_layout = [ { fname: "NAME", merge: "false", visible: "true" }, + { fname: "DATE", merge: "false", visible: "true" }, + { fname: "AGE", merge: "false", visible: "false" } ]; + + this.view_display(); + + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event("CHANGE_FLAG")) { + this.view_display(); + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_176.js b/srv/apps/z2ui5_cl_smp_app_176.js new file mode 100644 index 0000000..eb1ce65 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_176.js @@ -0,0 +1,122 @@ +// @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. +// @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"; + +const ty_s_data = { + name: "", + date: "", + age: "", +}; + +const ty_s_layout = { + fname: "", + title: "", + merge: "", + visible: "", + binding: "", +}; + +defineApp("Z2UI5_CL_SMP_APP_176", class { + + mt_layout = t.table(ty_s_layout); + mt_data = t.table(ty_s_data); + + main(client) { + + this.client = client; + if (client.check_on_init()) { + + this.mt_data = [ { name: "Theo", date: "01.01.2000", age: "5" }, + { name: "Lore", date: "01.01.2000", age: "1" } ]; + + this.mt_layout = [ { fname: "NAME", title: "Name", merge: "false", visible: "true", binding: "{NAME}" }, + { fname: "DATE", title: "Date", merge: "false", visible: "true", binding: "{DATE}" }, + { fname: "AGE", title: "Age", merge: "false", visible: "false", binding: "{AGE}" } ]; + + this.view_display(); + this.nest_view_display(); + + } else if (client.check_on_navigated()) { + + this.view_display(); + this.nest_view_display(); + + } + + } + + view_display() { + + const lo_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" }); + + const page = lo_view.ele("Shell") + .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" }); + + 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." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + this.client.view_display(lo_view.stringify()); + + } + + nest_view_display() { + + // the template model is the view model, so the list the repeat runs over + // is a bound attribute - its path is composed from the bind call, never + // written by hand + const layout_path = `{template>${this.client._bind({ val: "mt_layout", path: true })}}`; + + 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() + .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" }); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_186.js b/srv/apps/z2ui5_cl_smp_app_186.js new file mode 100644 index 0000000..93d74b4 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_186.js @@ -0,0 +1,105 @@ +// @keywords export save base64 attachment xstring document +// @summary Sends a file to the browser as a download - an xstring encoded as base64, handed over as an attachment. +// @docs https://abap2ui5.github.io/docs/cookbook/device_capabilities/upload_download +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_186.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_186", class { + + file_content_64 = ""; + file_name = ""; + + on_init() { + + this.file_name = "Default_File_Name.png"; + this.file_content_64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAA" + + "KYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIp" + + "QBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW" + + "0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpd" + + "mZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoeP" + + "PQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKc" + + "TNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jc" + + "W+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/" + + "JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3" + + "/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQj" + + "LYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII="; + + } + + on_event() { + + if (this.client.check_on_event("BUTTON_DOWNLOAD")) { + this.client.follow_up_action({ + val: this.client.cs_event.download_b64_file, + t_arg: [ this.file_content_64, this.file_name ] }); + } + + } + + 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" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - File - Download to the Browser" }) + .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: "The download_b64_file front-end action hands a base64 encoded file to the browser, which saves it " + + "under the given name - no ICF download service and no extra request needed." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele("FlexBox") + .a({ n: "width", v: "100%" }) + .a({ n: "height", v: "600px" }) + .a({ n: "alignItems", v: "Center" }) + .a({ n: "justifyContent", v: "SpaceAround" }) + .ele("VBox") + .tag("Text") + .a({ n: "text", v: "Base64 String:" }) + .tag("TextArea") + .a({ n: "value", v: this.client._bind("file_content_64") }) + .a({ n: "rows", v: "20" }) + .a({ n: "width", v: "800px" }) + .a({ n: "wrapping", v: "Soft" }) + .end() + .ele("VBox") + .a({ n: "justifyContent", v: "Center" }) + .a({ n: "alignItems", v: "Center" }) + .tag("Text") + .a({ n: "text", v: "fill filename:" }) + .tag("Input") + .a({ n: "value", v: this.client._bind("file_name") }) + .a({ n: "class", v: "sapUiLargeMarginBottom" }) + .a({ n: "width", v: "15rem" }) + .tag("Button") + .a({ n: "press", v: this.client._event("BUTTON_DOWNLOAD") }) + .a({ n: "text", v: "Download the file" }) + .a({ n: "type", v: "Emphasized" }); + + this.client.view_display(page.stringify()); + + } + + main(client) { + + this.client = client; + if (client.check_on_init()) { + this.on_init(); + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_189.js b/srv/apps/z2ui5_cl_smp_app_189.js new file mode 100644 index 0000000..10d7aa1 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_189.js @@ -0,0 +1,93 @@ +// @keywords cursor enter tab next field form set_focus +// @summary Moves the cursor to the next Input when Enter is pressed - the fast entry a form needs. +// @docs https://abap2ui5.github.io/docs/cookbook/browser_interaction/focus +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_189.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_189", class { + + one = ""; + two = ""; + three = ""; + + on_event() { + + switch (this.client.get_event()) { + case "one_enter": + this.client.follow_up_action({ + val: z2ui5_if_client.cs_event.set_focus, + t_arg: [ "IdTwo" ] }); + break; + case "two_enter": + this.client.follow_up_action({ + val: z2ui5_if_client.cs_event.set_focus, + t_arg: [ "IdThree" ] }); + break; + } + + } + + view_display() { + + const page = 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:form", v: "sap.ui.layout.form" }) + .ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Focus - Jump to the Next Input on Enter" }) + .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: "Pressing Enter in an input field jumps the cursor to the next one via the set_focus follow-up action." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }) + .tag("Label") + .a({ n: "text", v: "One (Press Enter)" }) + .tag("Input") + .a({ n: "id", v: "IdOne" }) + .a({ n: "value", v: this.client._bind("one") }) + .a({ n: "submit", v: this.client._event("one_enter") }) + .tag("Label") + .a({ n: "text", v: "Two" }) + .tag("Input") + .a({ n: "id", v: "IdTwo" }) + .a({ n: "value", v: this.client._bind("two") }) + .a({ n: "submit", v: this.client._event("two_enter") }) + .tag("Label") + .a({ n: "text", v: "Three" }) + .tag("Input") + .a({ n: "id", v: "IdThree" }) + .a({ n: "value", v: this.client._bind("three") }); + + this.client.view_display(page.stringify()); + + } + + main(client) { + + this.client = client; + if (client.check_on_init()) { + + this.view_display(); + client.follow_up_action({ + val: z2ui5_if_client.cs_event.set_focus, + t_arg: [ "IdOne" ] }); + + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_202.js b/srv/apps/z2ui5_cl_smp_app_202.js new file mode 100644 index 0000000..a6cfff4 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_202.js @@ -0,0 +1,119 @@ +// @keywords wizard step branching discardprogress setnextstep control_by_id +// @summary Drives a Wizard from the backend: setting the next step and discarding progress by ID, which is how a branching wizard is steered. +// @docs https://abap2ui5.github.io/docs/cookbook/event_navigation/frontend +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_202.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_202", class { + + next_step = ""; + + view_display(client) { + + let lr_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" }); + + lr_view = lr_view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Control Behaviour - Wizard with Steps" }) + .a({ n: "showNavButton", b: client.check_app_prev_stack() }) + .a({ n: "navButtonPress", v: client._event_nav_app_leave() }) + .a({ n: "id", v: "page_main" }); + + lr_view.tag("MessageStrip") + .a({ n: "text", v: "A sap.m.Wizard guides through numbered steps. Branching is enabled: " + + "step 2 offers two follow-up steps, and the button pressed there picks " + + "the branch - the backend calls discardProgress and setNextStep by id " + + "(follow_up_action with cs_event-control_by_id)." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + const lr_wizard = lr_view.ele("Wizard") + .a({ n: "id", v: "wiz" }) + .a({ n: "enableBranching", b: true }); + const lr_wiz_step1 = lr_wizard.ele("WizardStep") + .a({ n: "title", v: "STEP1" }) + .a({ n: "validated", b: true }) + .a({ n: "nextStep", v: "STEP2" }); + lr_wiz_step1.tag("MessageStrip") + .a({ n: "text", v: "STEP1" }); + + const lr_wiz_step2 = lr_wizard.ele("WizardStep") + .a({ n: "id", v: "STEP2" }) + .a({ n: "title", v: "STEP2" }) + .a({ n: "validated", b: true }) + .a({ n: "subsequentSteps", v: "STEP22, STEP23" }); + + lr_wiz_step2.tag("MessageStrip") + .a({ n: "text", v: "STEP2" }); + lr_wiz_step2.tag("Button") + .a({ n: "press", v: client._event("STEP22") }) + .a({ n: "text", v: "Press Step 2.2" }); + lr_wiz_step2.tag("Button") + .a({ n: "press", v: client._event("STEP23") }) + .a({ n: "text", v: "Press Step 2.3" }); + + const lr_wiz_step22 = lr_wizard.ele("WizardStep") + .a({ n: "id", v: "STEP22" }) + .a({ n: "title", v: "STEP2.2" }) + .a({ n: "validated", b: true }); + + lr_wiz_step22.tag("MessageStrip") + .a({ n: "text", v: "STEP22" }); + + const lr_wiz_step23 = lr_wizard.ele("WizardStep") + .a({ n: "id", v: "STEP23" }) + .a({ n: "title", v: "STEP2.3" }) + .a({ n: "validated", b: true }); + + lr_wiz_step23.tag("MessageStrip") + .a({ n: "text", v: "STEP23" }); + + const lr_wiz_step3 = lr_wizard.ele("WizardStep") + .a({ n: "title", v: "STEP3" }) + .a({ n: "validated", b: true }); + + lr_wiz_step3.tag("MessageStrip") + .a({ n: "text", v: "STEP3" }); + + client.view_display(lr_view.stringify()); + + // nextStep is an ASSOCIATION: no binding can carry it, and view_display( ) + // has just destroyed the slot XMLView.create rebuilds - so the branch the + // handler picked is gone from the fresh WizardStep while NEXT_STEP still + // describes it. Re-issuing the same call here is what makes the choice + // survive a navigation back, a draft restore or any later redisplay. + if (this.next_step !== "") { + client.follow_up_action({ + val: z2ui5_if_client.cs_event.control_by_id, + t_arg: [ "STEP2", "setNextStep", this.next_step ] }); + } + + } + + main(client) { + + if (client.check_on_navigated()) { + this.view_display(client); + } else if (client.check_on_event("STEP22") || client.check_on_event("STEP23")) { + + // the original wizard flow (discardProgress + setNextStep) as two + // generic whitelisted control calls - t_arg is positional: + // id, method, params (the step params are control ids; the view + // defaults to cs_view-main) + this.next_step = client.get_event(); + client.follow_up_action({ + val: z2ui5_if_client.cs_event.control_by_id, + t_arg: [ "wiz", "discardProgress", "STEP2" ] }); + client.follow_up_action({ + val: z2ui5_if_client.cs_event.control_by_id, + t_arg: [ "STEP2", "setNextStep", this.next_step ] }); + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_255.js b/srv/apps/z2ui5_cl_smp_app_255.js new file mode 100644 index 0000000..8c0de31 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_255.js @@ -0,0 +1,168 @@ +// @keywords flexbox layout responsive navigation tile panel +// @summary Lays a page out with FlexBox and custom CSS classes - tiles, panels and a QuickView popover, all from the view chain. +// @docs https://abap2ui5.github.io/docs/cookbook/view/definition +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_255.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_255", class { + + view_display() { + + // the classes below exist nowhere in UI5 - they are shipped with the view + // in the core:HTML style element and referenced by the class attribute of + // the controls, so a page can carry its own design + const css = ".navigationExamples .ne-flexbox1," + + ".navigationExamples .ne-flexbox2 \\{" + + " padding: 0;" + + "\\}" + + ".navigationExamples .ne-flexbox1 li \\{" + + " margin: 0.4em;" + + " padding: 0.4em 1.3em;" + + " list-style-type: none;" + + " text-align: center;" + + " background-color: #193441;" + + " cursor: pointer;" + + "\\}" + + ".navigationExamples .ne-flexbox1 li:hover \\{" + + " background-color: orange;" + + "\\}" + + ".navigationExamples .ne-flexbox2 li \\{" + + " margin: 0.5em;" + + " width: 25%;" + + " min-width: 15%;" + + " list-style-type: none;" + + " text-align: center;" + + " background-color: #193441;" + + " padding: 0.4em;" + + " transition: width 0.5s ease-out, background-color 0.5s ease-out, flex-basis 0.5s ease-out;" + + " cursor: pointer;" + + "\\}" + + ".navigationExamples .ne-flexbox2 li:hover \\{" + + " flex-basis: 35% !important;" + + " background-color: orange;" + + "\\}" + + ".navigationExamples .ne-flexbox1 li a," + + ".navigationExamples .ne-flexbox2 li a \\{" + + " color: #fff;" + + " text-decoration: none;" + + " font-size: 0.875rem;" + + "\\}"; + + 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" }); + // 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 + view.tag({ n: "HTML", ns: "core" }) + .a({ n: "content", v: "" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - CSS - FlexBox Layouts with Custom Classes" }) + .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: "Navigation layouts built with sap.m.FlexBox and CSS classes of this app's own: the " + + "stylesheet travels with the view, the controls name the classes. Variable width, equal width " + + "with a transition effect - the hint button in the header explains the panels." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele("headerContent") + .tag("Button") + .a({ n: "press", v: this.client._event("POPOVER") }) + .a({ n: "icon", v: "sap-icon://hint" }) + .a({ n: "id", v: "hint_icon" }) + .a({ n: "tooltip", v: "Sample information" }); + + page.ele("VBox") + .a({ n: "class", v: "navigationExamples" }) + .ele("Panel") + .a({ n: "headerText", v: "Variable width" }) + .ele("FlexBox") + .a({ n: "class", v: "ne-flexbox1" }) + .a({ n: "renderType", v: "List" }) + .a({ n: "alignItems", v: "Center" }) + .a({ n: "justifyContent", v: "Center" }) + .ele({ n: "HTML", ns: "core" }) + .a({ n: "content", v: "Item 1" }) + .end() + .ele({ n: "HTML", ns: "core" }) + .a({ n: "content", v: "Long item 2" }) + .end() + .ele({ n: "HTML", ns: "core" }) + .a({ n: "content", v: "Item 3" }) + .end() + .end() + .ele("Panel") + .a({ n: "headerText", v: "Same width, transition effect" }) + .ele("FlexBox") + .a({ n: "class", v: "ne-flexbox2" }) + .a({ n: "renderType", v: "List" }) + .a({ n: "alignItems", v: "Center" }) + .a({ n: "justifyContent", v: "SpaceBetween" }) + .ele({ n: "HTML", ns: "core" }) + .a({ n: "content", v: "Item 1" }) + .ele({ n: "layoutData", ns: "core" }) + .tag("FlexItemData") + .a({ n: "growFactor", v: "1" }) + .a({ n: "baseSize", v: "25%" }) + .end() + .end() + .ele({ n: "HTML", ns: "core" }) + .a({ n: "content", v: "Long item 2" }) + .ele({ n: "layoutData", ns: "core" }) + .tag("FlexItemData") + .a({ n: "growFactor", v: "1" }) + .a({ n: "baseSize", v: "25%" }) + .end() + .end() + .ele({ n: "HTML", ns: "core" }) + .a({ n: "content", v: "Item 3" }) + .ele({ n: "layoutData", ns: "core" }) + .tag("FlexItemData") + .a({ n: "growFactor", v: "1" }) + .a({ n: "baseSize", v: "25%" }) + .end() + .end(); + + this.client.view_display(view.stringify()); + + } + + popover_display(id) { + + const view = 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" }); + view.ele("QuickView") + .a({ n: "placement", v: "Bottom" }) + .a({ n: "width", v: "auto" }) + .ele("QuickViewPage") + .a({ n: "description", v: "The items are list entries in a FlexBox; the classes that shape and colour them " + + "are defined in the style element this view ships, not in the UI5 theme." }) + .a({ n: "header", v: "Sample information" }) + .a({ n: "pageId", v: "sampleInformationId" }); + + this.client.popover_display({ xml: view.stringify(), by_id: id }); + + } + + main(client) { + + this.client = client; + if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event("POPOVER")) { + this.popover_display("hint_icon"); + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_279.js b/srv/apps/z2ui5_cl_smp_app_279.js new file mode 100644 index 0000000..5ddea41 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_279.js @@ -0,0 +1,141 @@ +// @keywords dirty unsaved changes leave confirmation warning +// @summary Refuses to leave an app with unsaved changes: the confirmation popup in front of nav_app_leave, and how the dirty flag gets there. +// @docs https://abap2ui5.github.io/docs/cookbook/event_navigation/navigation/inner_app +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_279.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_279", class { + + text_input = ""; + dirty = false; + + main(client) { + + this.client = client; + if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + view_display() { + + const page = 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:tnt", v: "sap.tnt" }) + .a({ n: "xmlns:z2ui5", v: "z2ui5.cc" }) + .ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Navigation - Data Loss Protection on Leaving" }) + .a({ n: "showNavButton", b: this.client.check_app_prev_stack() }) + .a({ n: "navButtonPress", v: this.client._event("BACK") }); + + page.tag("MessageStrip") + .a({ n: "text", v: "Unsaved input marks the page dirty via a custom control; navigating back then opens a confirmation " + + "popup instead of leaving and losing the data." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + const box = page.ele("FlexBox") + .a({ n: "class", v: "sapUiTinyMargin" }) + .a({ n: "alignItems", v: "Start" }) + .a({ n: "direction", v: "Row" }); + + box.tag("Input") + .a({ n: "id", v: "input" }) + .a({ n: "placeholder", v: "Enter data, submit and navigate back to trigger data loss protection" }) + .a({ n: "value", v: this.client._bind("text_input") }) + .a({ n: "submit", v: this.client._event("SUBMIT") }) + .a({ n: "width", v: "40rem" }); + + box.ele({ n: "InfoLabel", ns: "tnt" }) + .a({ n: "class", v: "sapUiSmallMarginBegin sapUiTinyMarginTop" }) + .a({ n: "text", v: "dirty" }) + .a({ n: "colorScheme", v: "8" }) + .a({ n: "visible", v: this.client._bind("dirty") }); + + box.tag("Button") + .a({ n: "press", v: this.client._event("RESET") }) + .a({ n: "text", v: "Reset" }) + .a({ n: "visible", v: this.client._bind("dirty") }) + .a({ n: "class", v: "sapUiSmallMarginBegin" }); + + page.tag({ n: "Dirty", ns: "z2ui5" }); + + this.client.view_display(page.stringify()); + + this.client.follow_up_action({ + val: z2ui5_if_client.cs_event.set_focus, + t_arg: [ "input" ] }); + + } + + on_event() { + + switch (this.client.get_event()) { + case "BACK": + + if (this.dirty === true) { + this.popup_confirm_display(); + + } else { + this.client.nav_app_leave(); + } + break; + case "POPUP_LEAVE": + + this.client.popup_destroy(); + this.dirty = false; + this.client.nav_app_leave(); + break; + + case "POPUP_CANCEL": + this.client.popup_destroy(); + break; + case "SUBMIT": + this.dirty = (this.text_input !== ""); + break; + case "RESET": + + this.dirty = false; + this.text_input = ""; + break; + + } + + } + + popup_confirm_display() { + + const 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" }); + popup.ele("Dialog") + .a({ n: "title", v: "Warning" }) + .a({ n: "icon", v: "sap-icon://status-critical" }) + .ele("VBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("Text") + .a({ n: "text", v: "Your entries will be lost when you leave this page." }) + .end() + .ele("buttons") + .tag("Button") + .a({ n: "press", v: this.client._event("POPUP_CANCEL") }) + .a({ n: "text", v: "Cancel" }) + .tag("Button") + .a({ n: "press", v: this.client._event("POPUP_LEAVE") }) + .a({ n: "text", v: "Leave Page" }) + .a({ n: "type", v: "Emphasized" }); + + this.client.popup_display(popup.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_305.js b/srv/apps/z2ui5_cl_smp_app_305.js new file mode 100644 index 0000000..ea00a1d --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_305.js @@ -0,0 +1,125 @@ +// @keywords color background conditional formatting style data attribute +// @summary Colours single table cells from the backend: the row carries its colour as custom data and a stylesheet turns it into a background. +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_305.clas.abap +import { defineApp, t, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +const ty_s_row = { + title: "", + value: "", +}; + +defineApp("Z2UI5_CL_SMP_APP_305", class { + + t_tab = t.table(ty_s_row); + + set_view() { + + 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 - Color Table Cells from the Backend" }) + .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: "Table cells are coloured from the backend: each cell carries a data-color attribute bound to the " + + "row, and an inline html style element maps those values to a background colour." }) + .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 + page.tag({ n: "HTML", ns: "core" }) + .a({ n: "content", v: "" }); + + const tab = page.ele("Table") + .a({ n: "items", v: this.client._bind("t_tab") }) + .a({ n: "mode", v: "MultiSelect" }) + .ele("headerToolbar") + .ele("OverflowToolbar") + .tag("Title") + .a({ n: "text", v: "change cell color" }) + .end() + .end(); + + tab.ele("columns") + .ele("Column") + .tag("Text") + .a({ n: "text", v: "Title" }) + .end() + .ele("Column") + .tag("Text") + .a({ n: "text", v: "Color" }) + .end(); + + tab.ele("items") + .ele("ColumnListItem") + .ele("cells") + .ele("Text") + .a({ n: "text", v: "{TITLE}" }) + .ele("customData") + .tag({ n: "CustomData", ns: "core" }) + .a({ n: "value", v: "{VALUE}" }) + .a({ n: "key", v: "color" }) + .a({ n: "writeToDom", b: true }) + .end() + .end() + .tag("Input") + .a({ n: "enabled", b: true }) + .a({ n: "value", v: "{VALUE}" }); + + this.client.view_display(view.stringify()); + + } + + main(client) { + + this.client = client; + + if (client.check_on_init()) { + this.t_tab = [ + { title: "entry 01", value: "red" }, + { title: "entry 02", value: "blue" }, + { title: "entry 03", value: "green" }, + { title: "entry 04", value: "yellow" }, + { title: "entry 05", value: "orange" }, + { title: "entry 06", value: "grey" } ]; + + this.set_view(); + } else if (client.check_on_navigated()) { + this.set_view(); + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_316.js b/srv/apps/z2ui5_cl_smp_app_316.js new file mode 100644 index 0000000..c38eb25 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_316.js @@ -0,0 +1,173 @@ +// @keywords mailto tel sms urlhelper redirect native link +// @summary Opens mailto:, tel: and sms: links through URLHelper, so the device answers with its own mail or phone app instead of the browser. +// @docs https://abap2ui5.github.io/docs/cookbook/browser_interaction/url_handling +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_316.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_316", class { + + phone = ""; + mobile = ""; + + email = { + email: "", + subject: "", + body: "", + cc: "", + bcc: "", + }; + + url = { + url: "", + }; + + view_display(client) { + + this.url = { url: "http://www.sap.com" }; + this.email = { email: "email@email.com", + subject: "subject", + body: "body" }; + + const page = 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:form", v: "sap.ui.layout.form" }) + .a({ n: "xmlns:layout", v: "sap.ui.layout" }) + .ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Browser - Open Mail, Phone and SMS Links" }) + .a({ n: "showNavButton", b: client.check_app_prev_stack() }) + .a({ n: "navButtonPress", v: client._event_nav_app_leave() }); + + page.tag("MessageStrip") + .a({ n: "text", v: "The URL helper triggers native browser actions from ABAP: open e-mail, telephone and SMS links, or " + + "redirect the browser to a URL." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + const layout = page.ele({ n: "VerticalLayout", ns: "layout" }) + .a({ n: "class", v: "sapUiContentPadding" }) + .a({ n: "width", v: "100%" }); + + const email_form = layout.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Trigger E-Mail" }); + + email_form.tag("Label") + .a({ n: "text", v: "E-Mail" }) + .a({ n: "labelFor", v: "inputEmail" }); + email_form.tag("Input") + .a({ n: "id", v: "inputEmail" }) + .a({ n: "placeholder", v: "Enter email" }) + .a({ n: "type", v: "Email" }) + .a({ n: "value", v: client._bind("email-email") }) + .a({ n: "class", v: "sapUiSmallMarginBottom" }); + + email_form.tag("Input") + .a({ n: "id", v: "inputCcEmail" }) + .a({ n: "placeholder", v: "Enter cc email" }) + .a({ n: "type", v: "Email" }) + .a({ n: "value", v: client._bind("email-cc") }) + .a({ n: "class", v: "sapUiSmallMarginBottom" }); + + email_form.tag("Input") + .a({ n: "id", v: "inputBccEmail" }) + .a({ n: "placeholder", v: "Enter bcc email" }) + .a({ n: "type", v: "Email" }) + .a({ n: "value", v: client._bind("email-bcc") }) + .a({ n: "class", v: "sapUiSmallMarginBottom" }); + + email_form.tag("Label") + .a({ n: "text", v: "Subject" }) + .a({ n: "labelFor", v: "inputText" }); + email_form.tag("Input") + .a({ n: "id", v: "inputText" }) + .a({ n: "placeholder", v: "Enter text" }) + .a({ n: "value", v: client._bind("email-subject") }) + .a({ n: "class", v: "sapUiSmallMarginBottom" }); + + email_form.tag("Label") + .a({ n: "text", v: "Mail Body" }) + .tag("TextArea") + .a({ n: "value", v: client._bind("email-body") }) + .a({ n: "width", v: "100%" }) + .a({ n: "valueLiveUpdate", b: true }) + .a({ n: "growing", b: true }) + .a({ n: "growingMaxLines", v: "7" }); + + email_form.tag("Button") + .a({ n: "press", v: client.follow_up_action({ val: client.cs_event.urlhelper, + t_arg: [ "TRIGGER_EMAIL", + `$${client._bind("email")}` ] }) }) + .a({ n: "text", v: "Trigger Email" }); + + const telephone_form = layout.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Trigger Telephone" }); + + telephone_form.tag("Label") + .a({ n: "text", v: "Telephone" }) + .a({ n: "labelFor", v: "inputTel" }); + telephone_form.tag("Input") + .a({ n: "id", v: "inputTel" }) + .a({ n: "placeholder", v: "Enter telephone number" }) + .a({ n: "type", v: "Tel" }) + .a({ n: "value", v: client._bind("phone") }) + .a({ n: "class", v: "sapUiSmallMarginBottom" }); + telephone_form.tag("Button") + .a({ n: "press", v: client.follow_up_action({ val: client.cs_event.urlhelper, + t_arg: [ "TRIGGER_TEL", + `$${client._bind("phone")}` ] }) }) + .a({ n: "text", v: "Trigger Telephone" }); + + const mobile_form = layout.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Trigger SMS" }); + + mobile_form.tag("Label") + .a({ n: "text", v: "Number" }) + .a({ n: "labelFor", v: "inputNumber" }); + mobile_form.tag("Input") + .a({ n: "id", v: "inputNumber" }) + .a({ n: "placeholder", v: "Enter a number" }) + .a({ n: "type", v: "Number" }) + .a({ n: "value", v: client._bind("mobile") }) + .a({ n: "class", v: "sapUiSmallMarginBottom" }); + mobile_form.tag("Button") + .a({ n: "press", v: client.follow_up_action({ val: client.cs_event.urlhelper, + t_arg: [ "TRIGGER_SMS", `$${client._bind("mobile")}` ] }) }) + .a({ n: "text", v: "Trigger SMS" }); + + const url_form = layout.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Redirect" }); + url_form.tag("Label") + .a({ n: "text", v: "URL" }) + .a({ n: "labelFor", v: "inputUrl" }); + url_form.tag("Input") + .a({ n: "id", v: "inputUrl" }) + .a({ n: "placeholder", v: "Enter URL" }) + .a({ n: "type", v: "Url" }) + .a({ n: "value", v: client._bind("url-url") }) + .a({ n: "class", v: "sapUiSmallMarginBottom" }); + url_form.tag("Button") + .a({ n: "press", v: client.follow_up_action({ val: client.cs_event.urlhelper, + t_arg: [ "REDIRECT", `$${client._bind("url")}` ] }) }) + .a({ n: "text", v: "Redirect" }); + + client.view_display(page.stringify()); + + client.follow_up_action({ + val: z2ui5_if_client.cs_event.set_title, + t_arg: [ "URL Helper Sample" ] }); + + } + + main(client) { + + if (client.check_on_navigated()) { + this.view_display(client); + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_325.js b/srv/apps/z2ui5_cl_smp_app_325.js new file mode 100644 index 0000000..e926c15 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_325.js @@ -0,0 +1,121 @@ +// @keywords clipboard paste copy text area +// @summary Copies text into the browser clipboard from the backend: a follow-up action carries the string, a toast confirms what landed there. +// @docs https://abap2ui5.github.io/docs/cookbook/browser_interaction/clipboard +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_325.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_325", class { + + input = ""; + text = ""; + + main(client) { + + if (client.check_on_navigated()) { + + 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:uxap", v: "sap.uxap" }); + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Browser - Copy to Clipboard" }) + .a({ n: "showNavButton", b: client.check_app_prev_stack() }) + .a({ n: "navButtonPress", v: client._event_nav_app_leave() }); + + page.tag("MessageStrip") + .a({ n: "text", v: "Copy the input field or text-area content to the system clipboard via the clipboard_copy follow-up action." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + const obj_page = page.ele({ n: "ObjectPageLayout", ns: "uxap" }) + .a({ n: "showTitleInHeaderContent", b: true }) + .a({ n: "showEditHeaderButton", b: true }) + .a({ n: "upperCaseAnchorBar", b: false }); + + const header_title = obj_page.ele({ n: "headerTitle", ns: "uxap" }) + .ele({ n: "ObjectPageDynamicHeaderTitle", ns: "uxap" }); + + header_title.ele({ n: "expandedHeading", ns: "uxap" }) + .ele("HBox") + .tag("Title") + .a({ n: "text", v: "Test" }) + .a({ n: "wrapping", b: true }); + header_title.ele({ n: "snappedHeading", ns: "uxap" }) + .ele("FlexBox") + .a({ n: "alignItems", v: "Center" }) + .tag("Title") + .a({ n: "text", v: "Test" }) + .a({ n: "wrapping", b: true }); + + const sections = obj_page.ele({ n: "sections", ns: "uxap" }); + + sections.ele({ n: "ObjectPageSection", ns: "uxap" }) + .a({ n: "titleUppercase", b: false }) + .a({ n: "title", v: "..." }) + .a({ n: "id", v: "id_sec1" }) + .ele({ n: "subSections", ns: "uxap" }) + .ele({ n: "ObjectPageSubSection", ns: "uxap" }) + .a({ n: "id", v: "id_input" }) + .a({ n: "title", v: "Input field" }) + .ele({ n: "blocks", ns: "uxap" }) + .ele("VBox") + .tag("Input") + .a({ n: "value", v: client._bind("input") }) + .a({ n: "width", v: "50%" }) + .tag("Button") + .a({ n: "press", v: client._event("COPY_INPUT") }) + .a({ n: "text", v: "Copy input" }) + .a({ n: "type", v: "Emphasized" }); + + sections.ele({ n: "ObjectPageSection", ns: "uxap" }) + .a({ n: "titleUppercase", b: false }) + .a({ n: "title", v: "..." }) + .a({ n: "id", v: "id_sec2" }) + .ele({ n: "subSections", ns: "uxap" }) + .ele({ n: "ObjectPageSubSection", ns: "uxap" }) + .a({ n: "id", v: "id_text_area" }) + .a({ n: "title", v: "Text area" }) + .ele({ n: "blocks", ns: "uxap" }) + .ele("VBox") + .tag("Button") + .a({ n: "press", v: client._event("COPY_TEXT_AREA") }) + .a({ n: "text", v: "Copy text area" }) + .a({ n: "type", v: "Emphasized" }) + .tag("TextArea") + .a({ n: "value", v: client._bind("text") }) + .a({ n: "rows", v: "15" }) + .a({ n: "width", v: "100%" }) + .a({ n: "valueLiveUpdate", b: true }) + .a({ n: "editable", b: true }) + .a({ n: "id", v: "text_id" }) + .a({ n: "growing", b: true }) + .a({ n: "growingMaxLines", v: "50" }); + + client.view_display(view.stringify()); + + } + + switch (client.get_event()) { + case "COPY_INPUT": + client.follow_up_action({ + val: z2ui5_if_client.cs_event.clipboard_copy, + t_arg: [ this.input ] }); + client.message_toast_display(`input field copied: ${this.input}`); + break; + + case "COPY_TEXT_AREA": + client.follow_up_action({ + val: z2ui5_if_client.cs_event.clipboard_copy, + t_arg: [ this.text ] }); + client.message_toast_display(`text area copied: ${this.text}`); + break; + + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_327.js b/srv/apps/z2ui5_cl_smp_app_327.js new file mode 100644 index 0000000..2adcc1d --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_327.js @@ -0,0 +1,196 @@ +// @keywords localstorage sessionstorage persist store_data offline +// @summary Writes to the browser's local and session storage and reads it back, so a value survives a reload without any state in the backend. +// @docs https://abap2ui5.github.io/docs/cookbook/browser_interaction/clipboard +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_327.clas.abap +import { defineApp, t, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +// Both fields are strings on purpose. The value round-trips through the +// browser and back through the storage, so whatever sits under the key is +// outside this app's control - and the framework converts it into these +// components BEFORE main( ) runs, where no TRY/CATCH of ours can reach it. +// A numeric component would let one oversized stored value (or a leftover +// from an earlier shape) fail the conversion on every single app start, +// leaving no screen from which to clear it. +const ty_s_value = { + field1: "", + field2: "", +}; + +const ty_s_storage = { + type: "", + prefix: "", + key: "", + value: ty_s_value, +}; + +const ty_s_type = { + type: "", +}; + +defineApp("Z2UI5_CL_SMP_APP_327", class { + + s_storage = ty_s_storage; + s_stored_value = ty_s_value; + t_types = t.table(ty_s_type); + + main(client) { + + this.client = client; + if (client.check_on_init()) { + this.on_init(); + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + on_init() { + + this.t_types = [ { type: "local" }, { type: "session" } ]; + this.s_storage = { type: "local", + prefix: "prefix1", + key: "key1", + value: { field1: "1", + field2: "textfld1" } }; + + this.view_display(); + + } + + on_event() { + let lv_json; + + switch (this.client.get_event()) { + + case "LOCAL_STORAGE_LOADED": + // The z2ui5:Storage control read a value out of the browser storage + // and reports it through its `finished` event. The payload is a whole + // structure, so it arrives as JSON - here it is picked apart field by + // field, which is also what keeps this tolerant: whatever sits under + // the key may carry more (or other) fields than this app models - an + // earlier shape, or a value someone else wrote. A field that is not + // there simply stays empty. + lv_json = this.client.get_event_arg(4); + this.s_storage = { ...this.s_storage, value: { field1: this.json_get_value({ json: lv_json, + name: "FIELD1" }), + field2: this.json_get_value({ json: lv_json, + name: "FIELD2" }) } }; + break; + + case "GET_STORED_VALUE": + this.s_storage = { ...this.s_storage, value: this.s_stored_value }; + break; + + } + + } + + json_get_value({ json, name } = {}) { + let result = ""; + + // A minimal reader for one string field of a flat JSON object: find + // `"":"` and take what stands up to the next quote. The model writes + // the ABAP component names in upper case, hence the case-insensitive + // search. An app parsing arbitrary JSON wants a real parser instead. + const lv_marker = `"${name}":"`; + + const lv_off = json.toLowerCase().indexOf(lv_marker.toLowerCase()); + if (lv_off < 0) { + return result; + } + + result = ((json.substr(lv_off + lv_marker.length)).includes(("\"")) ? (json.substr(lv_off + lv_marker.length)).slice(0, (json.substr(lv_off + lv_marker.length)).indexOf(("\""))) : ""); + + return result; + } + + 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" }) + .a({ n: "xmlns:form", v: "sap.ui.layout.form" }) + .a({ n: "xmlns:z2ui5", v: "z2ui5.cc" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Browser - Local and Session Storage" }) + .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: "Reads and writes the browser's local or session storage. The " + + "value is a whole ABAP structure, not just a string: the write " + + "side sends it with the STORE_DATA frontend action, the invisible " + + "z2ui5:Storage control reads it back and reports it as JSON." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Local/Session Storage" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }) + .tag("Label") + .a({ n: "text", v: "Type" }) + .ele("Select") + .a({ n: "forceSelection", b: true }) + .a({ n: "selectedKey", v: this.client._bind("s_storage-type") }) + .a({ n: "items", v: this.client._bind("t_types") }) + .tag({ n: "Item", ns: "core" }) + .a({ n: "key", v: "{TYPE}" }) + .a({ n: "text", v: "{TYPE}" }) + .end() + .tag("Label") + .a({ n: "text", v: "Prefix" }) + .tag("Input") + .a({ n: "value", v: this.client._bind("s_storage-prefix") }) + .tag("Label") + .a({ n: "text", v: "Key" }) + .tag("Input") + .a({ n: "value", v: this.client._bind("s_storage-key") }) + .tag("Label") + .a({ n: "text", v: "Value - Field 1" }) + .tag("Input") + .a({ n: "type", v: "Number" }) + .a({ n: "value", v: this.client._bind("s_storage-value-field1") }) + .tag("Label") + .a({ n: "text", v: "Value - Field 2" }) + .tag("Input") + .a({ n: "value", v: this.client._bind("s_storage-value-field2") }) + .tag("Label") + .a({ n: "text", v: "" }) + .tag("Button") + .a({ n: "press", v: this.client.follow_up_action({ + val: z2ui5_if_client.cs_event.store_data, + t_arg: [ `$${this.client._bind("s_storage")}` ] }) }) + .a({ n: "text", v: "store" }) + .tag("Button") + .a({ n: "press", v: this.client._event("GET_STORED_VALUE") }) + .a({ n: "text", v: "get" }); + + // Invisible companion control: it reads `key` out of the selected storage + // and fires `finished` when the stored value differs from `value`. The + // comparison is by value, so a structure does not re-trigger on every + // render. + page.tag({ n: "Storage", ns: "z2ui5" }) + .a({ n: "finished", v: this.client._event({ val: "LOCAL_STORAGE_LOADED", + t_arg: [ "${$parameters>/type}", + "${$parameters>/prefix}", + "${$parameters>/key}", + "${$parameters>/value}" ] }) }) + .a({ n: "type", v: this.client._bind("s_storage-type") }) + .a({ n: "prefix", v: this.client._bind("s_storage-prefix") }) + .a({ n: "key", v: this.client._bind("s_storage-key") }) + .a({ n: "value", v: this.client._bind("s_stored_value") }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_361.js b/srv/apps/z2ui5_cl_smp_app_361.js new file mode 100644 index 0000000..06f1fb9 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_361.js @@ -0,0 +1,76 @@ +// @keywords logoff signout icf session end fiori launchpad +// @summary Ends the session from the client - the logoff an ICF session or a Fiori launchpad needs, triggered as a follow-up action. +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_361.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_361", class { + + main(client) { + + if (client.check_on_navigated()) { + + 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" }); + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Browser - Logout from the Client" }) + .a({ n: "showNavButton", b: client.check_app_prev_stack() }) + .a({ n: "navButtonPress", v: client._event_nav_app_leave() }); + + page.tag("MessageStrip") + .a({ n: "text", v: "follow_up_action( ) returned straight into the press attribute: the button carries SYSTEM_LOGOUT " + + "itself, so pressing it signs out on the client without ever reaching the backend. Inside a Fiori " + + "Launchpad the shell container handles the sign-out; otherwise the app navigates to the ICF logoff endpoint." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiMediumMargin" }) + .tag("Button") + .a({ n: "press", v: client.follow_up_action(client.cs_event.system_logout) }) + .a({ n: "text", v: "Logout (client)" }) + .a({ n: "icon", v: "sap-icon://log" }) + .a({ n: "type", v: "Reject" }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.tag("MessageStrip") + .a({ n: "text", v: "The same client-side call, but with an argument: t_arg passes the ICF logoff endpoint, here with a " + + "redirect to google.com appended. Still no backend roundtrip - the argument is baked into the view." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiMediumMargin" }) + .tag("Button") + .a({ n: "press", v: client.follow_up_action({ + val: client.cs_event.system_logout, + t_arg: [ "/sap/public/bc/icf/logoff?redirecturl=www.google.com" ] }) }) + .a({ n: "text", v: "Logout (client, with redirect)" }) + .a({ n: "icon", v: "sap-icon://log" }) + .a({ n: "type", v: "Reject" }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.tag("MessageStrip") + .a({ n: "text", v: "The other way round: _event( 'LOGOUT' ) makes the button call the backend, and follow_up_action( ) " + + "is invoked there on client - the identical SYSTEM_LOGOUT, only queued after the response renders. " + + "Take this route when the logout depends on backend logic, e.g. saving a draft first." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiMediumMargin" }) + .tag("Button") + .a({ n: "press", v: client._event("LOGOUT") }) + .a({ n: "text", v: "Logout (via backend)" }) + .a({ n: "icon", v: "sap-icon://log" }) + .a({ n: "type", v: "Reject" }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + client.view_display(view.stringify()); + + } else if (client.check_on_event("LOGOUT")) { + // same method as in the two buttons above, only called on client instead of + // returned into the view - the action is queued and runs once this response renders + client.follow_up_action(client.cs_event.system_logout); + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_382.js b/srv/apps/z2ui5_cl_smp_app_382.js new file mode 100644 index 0000000..7c027fd --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_382.js @@ -0,0 +1,136 @@ +// @keywords confirm warning error success information dialog action +// @summary Every MessageBox type - confirm, warning, error, success, information - and what a custom action button changes about the answer. +// @docs https://abap2ui5.github.io/docs/cookbook/translation_messages/message +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_382.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_382", class { + + title = ""; + message = ""; + details = ""; + + main(client) { + + this.client = client; + if (client.check_on_init()) { + this.on_init(); + this.view_display(); + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + on_init() { + + this.title = "abap2UI5"; + this.message = "This is a message box."; + this.details = "These are additional details about the message."; + + } + + on_event() { + + switch (this.client.get_event()) { + case "CUSTOM": + this.client.message_box_display({ + text: this.message, + title: this.title, + type: "information", + details: this.details, + actions: [ "Approve", "Reject" ], + emphasizedaction: "Approve" }); + break; + default: + this.client.message_box_display({ + text: this.message, + title: this.title, + type: this.client.get_event(), + details: this.details }); + break; + } + + } + + view_display() { + + const page = 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:form", v: "sap.ui.layout.form" }) + .ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Message - MessageBox, Types and Custom Actions" }) + .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: "This sample demonstrates MessageBox: open confirm, information, success, " + + "warning, error, or a custom dialog with your own actions." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele("headerContent") + .tag("Link") + .a({ n: "text", v: "UI5 Demo Kit" }) + .a({ n: "target", v: "_blank" }) + .a({ n: "href", v: "https://sdk.openui5.org/entity/sap.m.MessageBox/sample/sap.m.sample.MessageBox" }); + + page.ele("Panel") + .a({ n: "headerText", v: "Message Box Configuration" }) + .ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Settings" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }) + .tag("Label") + .a({ n: "text", v: "Title" }) + .tag("Input") + .a({ n: "value", v: this.client._bind("title") }) + .tag("Label") + .a({ n: "text", v: "Message" }) + .tag("Input") + .a({ n: "value", v: this.client._bind("message") }) + .tag("Label") + .a({ n: "text", v: "Details" }) + .tag("TextArea") + .a({ n: "value", v: this.client._bind("details") }) + .a({ n: "rows", v: "3" }); + + page.ele("footer") + .ele("OverflowToolbar") + .tag("Text") + .a({ n: "text", v: "Open Message Box:" }) + .tag("ToolbarSpacer") + .tag("Button") + .a({ n: "press", v: this.client._event("confirm") }) + .a({ n: "text", v: "Confirm" }) + .tag("Button") + .a({ n: "press", v: this.client._event("information") }) + .a({ n: "text", v: "Information" }) + .tag("Button") + .a({ n: "press", v: this.client._event("success") }) + .a({ n: "text", v: "Success" }) + .a({ n: "type", v: "Accept" }) + .tag("Button") + .a({ n: "press", v: this.client._event("warning") }) + .a({ n: "text", v: "Warning" }) + .tag("Button") + .a({ n: "press", v: this.client._event("error") }) + .a({ n: "text", v: "Error" }) + .a({ n: "type", v: "Reject" }) + .tag("Button") + .a({ n: "press", v: this.client._event("CUSTOM") }) + .a({ n: "text", v: "Custom" }) + .a({ n: "type", v: "Emphasized" }); + + this.client.view_display(page.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_445.js b/srv/apps/z2ui5_cl_smp_app_445.js new file mode 100644 index 0000000..471c2b5 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_445.js @@ -0,0 +1,170 @@ +// @keywords sap.ui.device responsive orientation resize media model +// @summary Reads the sap.ui.Device model - phone, tablet or desktop, orientation and resize - so a view can react to the device it is on. +// @docs https://abap2ui5.github.io/docs/cookbook/model/device_model https://abap2ui5.github.io/docs/cookbook/device_capabilities/info +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_445.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_445", class { + + main(client) { + + this.client = client; + if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + on_event() { + + if (this.client.check_on_event("OPEN_POPUP")) { + this.popup_display(); + } + + } + + // Build the label/value rows that show the live device state. The + // `device>` model is a OneWay JSONModel over sap.ui.Device, so the + // bindings below update on their own when the window is resized or the + // device is rotated - no backend round-trip is involved. + device_form(parent) { + let result = null; + + const form = parent.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "layout", v: "ResponsiveGridLayout" }) + .a({ n: "editable", b: false }); + + // a readable label per system type instead of the raw booleans + form.tag("Label") + .a({ n: "text", v: "System type" }) + .ele("ObjectStatus") + .a({ n: "state", v: "Information" }) + .a({ n: "text", v: "{= ${device>/system/phone} ? 'Phone' : (${device>/system/tablet} ? 'Tablet' : (${device>/system/desktop} ? 'Desktop' : 'Other')) }" }); + + form.tag("Label") + .a({ n: "text", v: "Orientation" }) + .ele("ObjectStatus") + .a({ n: "text", v: "{= ${device>/orientation/landscape} ? 'Landscape' : 'Portrait' }" }); + + // resize/width and resize/height are updated live by UI5 + form.tag("Label") + .a({ n: "text", v: "Window size" }) + .ele("ObjectStatus") + .a({ n: "text", v: "{device>/resize/width} x {device>/resize/height} px" }); + + form.tag("Label") + .a({ n: "text", v: "Touch support" }) + .ele("ObjectStatus") + .a({ n: "state", v: "{= ${device>/support/touch} ? 'Success' : 'None' }" }) + .a({ n: "text", v: "{= ${device>/support/touch} ? 'Yes' : 'No' }" }); + + form.tag("Label") + .a({ n: "text", v: "Browser" }) + .tag("Text") + .a({ n: "text", v: "{device>/browser/name} {device>/browser/version}" }); + + form.tag("Label") + .a({ n: "text", v: "Operating system" }) + .tag("Text") + .a({ n: "text", v: "{device>/os/name} {device>/os/version}" }); + + result = form; + + return result; + } + + 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" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Device - Device Model: Phone, Tablet, Desktop" }) + .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: "The 'device>' model is a one-way JSONModel over sap.ui.Device. " + + "Resize the window or rotate your device and the values update live - " + + "no backend round-trip. It is available in this view and in the dialog below." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + // 1) the raw device state, bound field by field + this.device_form(page.ele("Panel") + .a({ n: "class", v: "sapUiSmallMargin" }) + .a({ n: "headerText", v: "Live device properties" })); + + // 2) an expression binding that reacts to the device type + page.tag("MessageStrip") + .a({ n: "text", v: "{= ${device>/system/phone} ? 'Compact layout - you are on a phone.' : 'Full layout - tablet or desktop.' }" }) + .a({ n: "type", v: "{= ${device>/system/phone} ? 'Warning' : 'Success' }" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + // 3) a control whose content collapses on a phone: expanded = !phone + const tabs = page.ele("Panel") + .a({ n: "class", v: "sapUiSmallMargin" }) + .a({ n: "headerText", v: "Responsive IconTabBar (expanded only when it is not a phone)" }) + .ele("IconTabBar") + .a({ n: "class", v: "sapUiResponsiveContentPadding" }) + .a({ n: "expanded", v: "{= !${device>/system/phone} }" }) + .ele("items"); + + tabs.ele("IconTabFilter") + .a({ n: "icon", v: "sap-icon://money-bills" }) + .a({ n: "text", v: "Sales" }) + .a({ n: "key", v: "sales" }) + .tag("Text") + .a({ n: "text", v: "On a phone the tab content is collapsed to save space; on tablet/desktop it stays expanded." }); + + tabs.ele("IconTabFilter") + .a({ n: "icon", v: "sap-icon://product" }) + .a({ n: "text", v: "Stock" }) + .a({ n: "key", v: "stock" }) + .tag("Text") + .a({ n: "text", v: "Everything here is driven purely by the device> model - no event handler." }); + + // 4) the same device state, but inside a popup (device> now reaches popups too) + page.tag("Button") + .a({ n: "press", v: this.client._event("OPEN_POPUP") }) + .a({ n: "text", v: "Open dialog (device model inside a popup)" }) + .a({ n: "icon", v: "sap-icon://sys-monitor" }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + this.client.view_display(view.stringify()); + + } + + popup_display() { + + const 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" }); + + // the dialog width itself is driven by the device model + const dialog = popup.ele("Dialog") + .a({ n: "title", v: "Device model inside a popup" }) + .a({ n: "contentWidth", v: "{= ${device>/system/phone} ? '95%' : '420px' }" }); + + this.device_form(dialog.ele("content")); + + dialog.ele("buttons") + .tag("Button") + .a({ n: "press", v: this.client.follow_up_action(this.client.cs_event.popup_close) }) + .a({ n: "text", v: "Close" }) + .a({ n: "type", v: "Emphasized" }); + + this.client.popup_display(popup.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_448.js b/srv/apps/z2ui5_cl_smp_app_448.js new file mode 100644 index 0000000..4455e7a --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_448.js @@ -0,0 +1,82 @@ +// @keywords panel collapse expand setexpanded control_by_id whitelisted +// @summary Expands a Panel by calling setExpanded on it by ID - a whitelisted control call, no roundtrip and no model behind it. +// @docs https://abap2ui5.github.io/docs/cookbook/event_navigation/frontend +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_448.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_448", class { + + // not bound - mirrors the panel state so the toggle can invert it + expanded = false; + + main(client) { + + this.client = client; + if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + on_event() { + + if (this.client.get_event() === "TOGGLE") { + // invert the mirrored state and call the whitelisted setExpanded on + // the panel - client-side, after the response renders, no rebuild. + // t_arg is positional: id, method, params (the view defaults to + // cs_view-main and can be omitted for a main-view control) + this.expanded = (this.expanded === false); + // Driving a property through control_by_id IS this sample; the plain + // binding the rule recommends is what z2ui5_cl_smp_app_449 shows instead. + this.client.follow_up_action({ val: z2ui5_if_client.cs_event.control_by_id, + t_arg: [ "demoPanel", + "setExpanded", + (this.expanded ? "X" : "") ] }); + } + + } + + 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" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Control Behaviour - Expand a Panel by ID (setExpanded)" }) + .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: "The button toggles the panel via the whitelisted setExpanded method " + + "(follow_up_action with cs_event-control_by_id), client-side after render - no view rebuild." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele("VBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("Button") + .a({ n: "press", v: this.client._event("TOGGLE") }) + .a({ n: "text", v: "Toggle panel" }) + .a({ n: "icon", v: "sap-icon://expand-group" }); + + page.ele("Panel") + .a({ n: "expandable", b: true }) + .a({ n: "width", v: "auto" }) + .a({ n: "id", v: "demoPanel" }) + .a({ n: "class", v: "sapUiSmallMargin" }) + .a({ n: "headerText", v: "Collapsible panel" }) + .tag("Text") + .a({ n: "text", v: "Content of the panel - collapsed and expanded from the backend without a roundtrip payload." }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_449.js b/srv/apps/z2ui5_cl_smp_app_449.js new file mode 100644 index 0000000..1d2cc7a --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_449.js @@ -0,0 +1,75 @@ +// @keywords pdfviewer pdf document viewer popup control_by_id whitelisted +// @summary Opens the PDF viewer by calling it by ID, so a document can be shown from an event without rebuilding the view. +// @docs https://abap2ui5.github.io/docs/cookbook/device_capabilities/pdf https://abap2ui5.github.io/docs/cookbook/event_navigation/frontend +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_449.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_449", class { + + main(client) { + + this.client = client; + if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + on_event() { + + if (this.client.get_event() === "OPEN") { + // open the popup-mode PDFViewer via the whitelisted open method - + // the viewer brings its own dialog and close button. + // t_arg is positional: id, method (the view defaults to cs_view-main + // and can be omitted for a main-view control) + this.client.follow_up_action({ val: z2ui5_if_client.cs_event.control_by_id, + t_arg: [ "demoPdf", + "open" ] }); + } + + } + + 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" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Control Behaviour - Open the PDF Viewer by ID" }) + .a({ n: "showNavButton", b: this.client.check_app_prev_stack() }) + .a({ n: "navButtonPress", v: this.client._event_nav_app_leave() }); + + // a popup-mode PDFViewer kept as a dependent of the page; opened + // imperatively from the backend, like a controller calling oViewer.open() + page.ele("dependents") + .ele("PDFViewer") + .a({ n: "id", v: "demoPdf" }) + .a({ n: "title", v: "Sample PDF" }) + .a({ n: "source", v: "https://sapui5.hana.ondemand.com/test-resources/sap/m/demokit/sample/PDFViewerPopup/sample1.pdf" }) + .a({ n: "height", v: "100%" }); + + page.tag("MessageStrip") + .a({ n: "text", v: "The button opens the popup-mode PDFViewer via the whitelisted open method " + + "(follow_up_action with cs_event-control_by_id), client-side after render." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele("VBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("Button") + .a({ n: "press", v: this.client._event("OPEN") }) + .a({ n: "text", v: "Open PDF" }) + .a({ n: "icon", v: "sap-icon://pdf-attachment" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_450.js b/srv/apps/z2ui5_cl_smp_app_450.js new file mode 100644 index 0000000..6424afe --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_450.js @@ -0,0 +1,95 @@ +// @keywords dats tims conversion initial date 00000000 sy-datum +// @summary ABAP DATS and TIMS strings in the view: the conversion in both directions, including what an initial 00000000 has to become. +// @docs https://abap2ui5.github.io/docs/cookbook/model/formatter +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_450.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_450", class { + + dats = ""; + tims = ""; + dats_initial = ""; + + main(client) { + + this.client = client; + if (client.check_on_init()) { + // the 8/6-character forms an ABAP DATS/TIMS value has when it travels + // as a string (a CHAR(8) key, a legacy structure field). A field typed + // d or t is serialized as ISO by the framework and needs + // Formatter.DateCreateObject instead - see z2ui5_cl_smp_app_457 + this.dats = "20260720"; + this.tims = "134501"; + this.dats_initial = "00000000"; + this.view_display(); + } else if (client.check_on_navigated()) { + this.view_display(); + } + + } + + 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" }) + .a({ n: "xmlns:form", v: "sap.ui.layout.form" }); + + // require the framework's curated formatter module into the view, like an + // original UI5 app requires its model/formatter. DateAbapDateToDateObject + // and DateAbapDateTimeToDateObject turn the ABAP date strings into the + // real JS Date that an object-typed property (dateValue) demands - JSON + // has no date type, so this is the one conversion the backend cannot do. + view.a({ n: "core:require", v: "{Formatter: 'z2ui5/model/formatter'}" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Formatter - ABAP Date and Time Strings (DATS/TIMS)" }) + .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: "The model carries the plain ABAP strings 20260720 / 134501; the curated formatter " + + "converts them at the binding. An initial DATS (00000000) yields null, so the field " + + "stays empty instead of rendering a wrong 1899 date." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + // the paths must come from a bind call - a hardcoded path is never + // registered in the model and the frontend receives no data for it + page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "DATS / TIMS strings as date objects" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }) + .tag("Label") + .a({ n: "text", v: "DATS 20260720" }) + .tag("DatePicker") + .a({ n: "displayFormat", v: "long" }) + .a({ n: "dateValue", v: `{ path: '${this.client._bind({ val: "dats", path: true })}', ` + + `formatter: 'Formatter.DateAbapDateToDateObject' }` }) + .a({ n: "editable", b: false }) + .tag("Label") + .a({ n: "text", v: "DATS 00000000 (initial)" }) + .tag("DatePicker") + .a({ n: "displayFormat", v: "long" }) + .a({ n: "placeholder", v: "no date" }) + .a({ n: "dateValue", v: `{ path: '${this.client._bind({ val: "dats_initial", path: true })}', ` + + `formatter: 'Formatter.DateAbapDateToDateObject' }` }) + .a({ n: "editable", b: false }) + .tag("Label") + .a({ n: "text", v: "DATS 20260720 + TIMS 134501" }) + .ele("DateTimePicker") + .a({ n: "editable", v: "false" }) + .a({ n: "dateValue", v: `{ parts: [{path: '${this.client._bind({ val: "dats", path: true })}'}, ` + + `{path: '${this.client._bind({ val: "tims", path: true })}'}], ` + + `formatter: 'Formatter.DateAbapDateTimeToDateObject' }` }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_456.js b/srv/apps/z2ui5_cl_smp_app_456.js new file mode 100644 index 0000000..2145d1c --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_456.js @@ -0,0 +1,103 @@ +// @keywords planningcalendar appointment javascript date object iso +// @summary Appointments in a PlanningCalendar: start and end as date objects, which is what the control binds against. +// @docs https://abap2ui5.github.io/docs/cookbook/model/formatter +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_456.clas.abap +import { defineApp, t, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +const ty_s_appointment = { + start_at: "", + end_at: "", + title: "", + type: "", +}; + +const ty_s_person = { + name: "", + t_appointments: t.table(ty_s_appointment), +}; + +defineApp("Z2UI5_CL_SMP_APP_456", class { + + t_people = t.table(ty_s_person); + start_date = ""; + + main(client) { + + this.client = client; + if (client.check_on_init()) { + this.start_date = "2026-07-20T07:00:00"; + this.t_people = [ + { name: "Anna Miller", + t_appointments: [ + { start_at: "2026-07-20T08:00:00", end_at: "2026-07-20T09:00:00", + title: "Team meeting", type: "Type01" }, + { start_at: "2026-07-20T11:00:00", end_at: "2026-07-20T12:30:00", + title: "Customer call", type: "Type08" } ] }, + { name: "Tom Schmidt", + t_appointments: [ + { start_at: "2026-07-20T09:30:00", end_at: "2026-07-20T10:30:00", + title: "Code review", type: "Type06" } ] } ]; + this.view_display(); + } else if (client.check_on_navigated()) { + this.view_display(); + } + + } + + 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" }) + .a({ n: "xmlns:u", v: "sap.ui.unified" }); + + // calendar date properties (CalendarAppointment startDate/endDate, + // PlanningCalendar startDate) are typed "object" - they demand a real JS + // Date; a plain string binding crashes view creation ("Date must be a + // JavaScript or UI5Date date object"). Formatter.DateCreateObject from + // the curated module converts the model's ISO strings at the point of + // use - the model itself stays plain strings everywhere. + view.a({ n: "core:require", v: "{Formatter: 'z2ui5/model/formatter'}" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Formatter - Date Objects for the PlanningCalendar" }) + .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: "The model carries plain ISO strings; Formatter.DateCreateObject turns them into " + + "the real JS Date objects the object-typed calendar properties require - only at " + + "the bindings that need them." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + // the startDate path must come from _bind - a hardcoded binding + // path is never registered in the model, the frontend then receives no + // data and the formatter passes a non-Date into the object property + page.ele("PlanningCalendar") + .a({ n: "rows", v: this.client._bind("t_people") }) + .a({ n: "startDate", v: `{ path: '${this.client._bind({ val: "start_date", path: true })}', ` + + `formatter: 'Formatter.DateCreateObject' }` }) + .a({ n: "id", v: "PC1" }) + .a({ n: "class", v: "sapUiSmallMargin" }) + .ele("rows") + .ele("PlanningCalendarRow") + .a({ n: "appointments", v: "{path: 'T_APPOINTMENTS', templateShareable: true}" }) + .a({ n: "title", v: "{NAME}" }) + .ele("appointments") + .ele({ n: "CalendarAppointment", ns: "u" }) + .a({ n: "startDate", v: "{ path: 'START_AT', formatter: 'Formatter.DateCreateObject' }" }) + .a({ n: "endDate", v: "{ path: 'END_AT', formatter: 'Formatter.DateCreateObject' }" }) + .a({ n: "title", v: "{TITLE}" }) + .a({ n: "type", v: "{TYPE}" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_457.js b/srv/apps/z2ui5_cl_smp_app_457.js new file mode 100644 index 0000000..95e24d1 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_457.js @@ -0,0 +1,69 @@ +// @keywords datepicker datevalue javascript date object iso +// @summary The DatePicker's dateValue wants a JavaScript date object rather than a string - what that means for the binding of an ABAP date. +// @docs https://abap2ui5.github.io/docs/cookbook/model/formatter +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_457.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_457", class { + + date_iso = ""; + + main(client) { + + this.client = client; + if (client.check_on_init()) { + this.date_iso = "2026-07-20"; + this.view_display(); + } else if (client.check_on_navigated()) { + this.view_display(); + } + + } + + 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" }); + + // the minimal date-object case: DatePicker.dateValue is typed "object" + // and demands a real JS Date - a plain string binding crashes view + // creation. Formatter.DateCreateObject converts the model's ISO string + // at this one binding; the model itself keeps the plain string (the + // Text below proves it). + view.a({ n: "core:require", v: "{Formatter: 'z2ui5/model/formatter'}" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Formatter - Date Object for the DatePicker" }) + .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: "dateValue is an object-typed property: the ISO string from the model becomes a " + + "real JS Date via Formatter.DateCreateObject - only at this binding, the model " + + "stays a plain string." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + // the path must come from _bind - a hardcoded binding path is never + // registered in the model and the frontend receives no data for it + page.ele("VBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("DatePicker") + .a({ n: "displayFormat", v: "long" }) + .a({ n: "dateValue", v: `{ path: '${this.client._bind({ val: "date_iso", path: true })}', ` + + `formatter: 'Formatter.DateCreateObject' }` }) + .tag("Text") + .a({ n: "text", v: `Model value (unchanged string): ${this.client._bind("date_iso")}` }) + .a({ n: "class", v: "sapUiTinyMarginTop" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_460.js b/srv/apps/z2ui5_cl_smp_app_460.js new file mode 100644 index 0000000..0d48c13 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_460.js @@ -0,0 +1,80 @@ +// @keywords hierarchy nodes nested json items +// @summary A nested ABAP table rendered as a sap.m.Tree - the hierarchy comes from the data, not from the view. +// @docs https://abap2ui5.github.io/docs/cookbook/model/trees +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_460.clas.abap +import { defineApp, t, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +const ty_s_node_level3 = { + text: "", +}; + +const ty_s_node_level2 = { + text: "", + nodes: t.table(ty_s_node_level3), +}; + +const ty_s_node_level1 = { + text: "", + nodes: t.table(ty_s_node_level2), +}; + +defineApp("Z2UI5_CL_SMP_APP_460", class { + + t_nodes = t.table(ty_s_node_level1); + + main(client) { + + this.client = client; + if (client.check_on_init()) { + this.t_nodes = [ + { text: "Documents", nodes: [ + { text: "Projects", nodes: [ + { text: "Roadmap.docx" }, + { text: "Budget.xlsx" } ] }, + { text: "Reports", nodes: [ + { text: "Q1.pdf" }, + { text: "Q2.pdf" } ] } ] }, + { text: "Pictures", nodes: [ + { text: "Vacation", nodes: [ + { text: "Beach.jpg" } ] } ] }, + { text: "Music" } ]; + this.view_display(); + } else if (client.check_on_navigated()) { + this.view_display(); + } + + } + + 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" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Tree - Nested ABAP Table in a sap.m.Tree" }) + .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 nested ABAP table (three levels of NODES) serializes into nested JSON arrays; " + + "sap.m.Tree binds them directly - no flattening, no extra code." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele("Tree") + .a({ n: "id", v: "tree1" }) + .a({ n: "items", v: this.client._bind("t_nodes") }) + .a({ n: "headerText", v: "Files" }) + .tag("StandardTreeItem") + .a({ n: "title", v: "{TEXT}" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_462.js b/srv/apps/z2ui5_cl_smp_app_462.js new file mode 100644 index 0000000..701c5f4 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_462.js @@ -0,0 +1,134 @@ +// @keywords popup expand state hierarchy nodes +// @summary A tree inside a dialog, including which nodes stay expanded when the popup is opened again. +// @docs https://abap2ui5.github.io/docs/cookbook/model/trees +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_462.clas.abap +import { defineApp, t, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +const ty_s_node_level3 = { + text: "", +}; + +const ty_s_node_level2 = { + text: "", + nodes: t.table(ty_s_node_level3), +}; + +const ty_s_node_level1 = { + text: "", + nodes: t.table(ty_s_node_level2), +}; + +defineApp("Z2UI5_CL_SMP_APP_462", class { + + t_nodes = t.table(ty_s_node_level1); + + main(client) { + + this.client = client; + if (client.check_on_init()) { + this.t_nodes = [ + { text: "Sales", nodes: [ + { text: "Orders", nodes: [ + { text: "4711 - Notebook Basic" }, + { text: "4712 - Ergo Screen" } ] }, + { text: "Quotations", nodes: [ + { text: "Q-001 - ITelO Vault" } ] } ] }, + { text: "Purchasing", nodes: [ + { text: "Suppliers", nodes: [ + { text: "Very Best Screens" } ] } ] } ]; + this.view_display(); + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + on_event() { + + switch (this.client.get_event()) { + + case "OPEN_POPUP": + this.popup_display(); + break; + + case "CLOSE_POPUP": + // closing goes through the backend ON PURPOSE: the z2ui5.cc.Tree + // companion snapshots the expand state right before every roundtrip, + // so this event captures it while the dialog still exists - a pure + // client-side popup_close would skip the snapshot + this.client.popup_destroy(); + break; + + } + + } + + popup_display() { + + const 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" }) + .a({ n: "xmlns:z2ui5", v: "z2ui5.cc" }); + const dialog = popup.ele("Dialog") + .a({ n: "title", v: "abap2UI5 - Tree - Inside a Dialog" }); + + // the popup view slot gets its own copy of the model - the nested table + // bound here renders in the dialog exactly like in a main view + dialog.ele("Tree") + .a({ n: "id", v: "treePopup" }) + .a({ n: "items", v: this.client._bind("t_nodes") }) + .a({ n: "headerText", v: "Documents" }) + .tag("StandardTreeItem") + .a({ n: "title", v: "{TEXT}" }); + + // invisible companion: snapshots the tree's expand state before each + // roundtrip and re-applies it after rendering - reopening the dialog + // shows the same nodes expanded as when it was closed + dialog.tag({ n: "Tree", ns: "z2ui5" }); + + dialog.ele("buttons") + .tag("Button") + .a({ n: "press", v: this.client._event("CLOSE_POPUP") }) + .a({ n: "text", v: "Close" }); + + this.client.popup_display(popup.stringify()); + + } + + 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" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Tree - Inside a Dialog" }) + .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: "The button opens a Dialog whose content is a sap.m.Tree over a nested ABAP " + + "table. Expand some nodes, close and reopen: the z2ui5.cc.Tree companion " + + "preserves the expand state across the roundtrips." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele("VBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("Button") + .a({ n: "press", v: this.client._event("OPEN_POPUP") }) + .a({ n: "text", v: "Open tree popup" }) + .a({ n: "icon", v: "sap-icon://tree" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_465.js b/srv/apps/z2ui5_cl_smp_app_465.js new file mode 100644 index 0000000..04721c1 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_465.js @@ -0,0 +1,83 @@ +// @keywords toggleby open close control_by_id whitelisted +// @summary Opens and closes a Popover by ID (toggleBy), so the anchor decides and no roundtrip is needed. +// @docs https://abap2ui5.github.io/docs/cookbook/popup_popover/popover https://abap2ui5.github.io/docs/cookbook/event_navigation/frontend +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_465.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_465", class { + + main(client) { + + this.client = client; + if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + on_event() { + + if (this.client.get_event() === "TOGGLE") { + // toggle the popover open/closed, anchored to the pressed button's DOM + // ref - the whitelisted toggleBy opens it if closed, closes it if open + // (the controller pattern oPopover.openBy(oButton) / oPopover.close()). + // t_arg is positional: id, method, anchor id (the view defaults to + // cs_view-main and can be omitted for a main-view control) + this.client.follow_up_action({ val: z2ui5_if_client.cs_event.control_by_id, + t_arg: [ "demoPopover", + "toggleBy", + this.client.get_event_arg() ] }); + } + + } + + 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" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Popover - Toggle by ID (toggleBy)" }) + .a({ n: "showNavButton", b: this.client.check_app_prev_stack() }) + .a({ n: "navButtonPress", v: this.client._event_nav_app_leave() }); + + // the popover kept as a dependent of the page; opened and closed + // imperatively from the backend, anchored to the button that fired + page.ele("dependents") + .ele("Popover") + .a({ n: "id", v: "demoPopover" }) + .a({ n: "title", v: "Details" }) + .a({ n: "placement", v: "Bottom" }) + .a({ n: "contentWidth", v: "18rem" }) + .tag("Text") + .a({ n: "text", v: "Toggled open and closed from the backend - the same button opens " + + "it when closed and closes it when open, no view rebuild and no payload." }) + .end(); + + page.tag("MessageStrip") + .a({ n: "text", v: "The button toggles the popover via the whitelisted toggleBy method " + + "(follow_up_action with cs_event-control_by_id), anchored to the button's DOM ref " + + "passed as $event.oSource.sId - open-if-closed, close-if-open, client-side after render." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele("VBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("Button") + .a({ n: "press", v: this.client._event({ val: "TOGGLE", + arg: "$event.oSource.sId" }) }) + .a({ n: "text", v: "Toggle popover" }) + .a({ n: "icon", v: "sap-icon://email" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_466.js b/srv/apps/z2ui5_cl_smp_app_466.js new file mode 100644 index 0000000..e531aba --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_466.js @@ -0,0 +1,71 @@ +// @keywords icon glyph placeholder text status expandinlineicons +// @summary Puts icons inside a text through placeholders that UI5 expands, so a status line can carry a glyph without its own control. +// @docs https://abap2ui5.github.io/docs/cookbook/model/formatter +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_466.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_466", class { + + status_text = ""; + + main(client) { + + this.client = client; + if (client.check_on_init()) { + + this.status_text = "Deployment successful! %%icon:sap-icon://message-success%% All services " + + "%%icon:sap-icon://sys-enter-2%% are running. Check status " + + "%%icon:sap-icon://stethoscope%%"; + + this.view_display(); + } else if (client.check_on_navigated()) { + this.view_display(); + + } + + } + + 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" }); + + // require the framework's curated formatter module into the view - + // expandInlineIcons is the sap.m.MessageStripUtilities.getInlineIcon() + // equivalent: a whole-string formatter that replaces every + // %%icon:sap-icon://%% placeholder with inline-icon markup (the + // glyph resolved via IconPool), so the app never hardcodes icon-font + // codepoints. Rendered by MessageStrip with enableFormattedText. + view.a({ n: "core:require", v: "{Formatter: 'z2ui5/model/formatter'}" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Formatter - Inline Icons in a Text" }) + .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: "The status line below binds a plain string carrying %%icon:sap-icon://...%% placeholders " + + "through Formatter.expandInlineIcons - each placeholder becomes an inline icon glyph, " + + "no codepoints in the app." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.tag("MessageStrip") + .a({ n: "text", v: `{ path: '${this.client._bind({ val: "status_text", path: true })}', ` + + `formatter: 'Formatter.expandInlineIcons' }` }) + .a({ n: "type", v: "Success" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }) + .a({ n: "enableFormattedText", b: true }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_467.js b/srv/apps/z2ui5_cl_smp_app_467.js new file mode 100644 index 0000000..f2a2af0 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_467.js @@ -0,0 +1,109 @@ +// @keywords messagemanager validation target field state central model +// @summary The UI5 message model: validation messages carry the field they belong to, so the control shows the state and one list holds them all. +// @docs https://abap2ui5.github.io/docs/cookbook/translation_messages/message +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_467.clas.abap +import { defineApp, t, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +const ty_s_message = { + message: "", + description: "", + type: "", + target: "", + additionaltext: "", +}; + +defineApp("Z2UI5_CL_SMP_APP_467", class { + + t_messages = t.table(ty_s_message); + name = ""; + amount = 0; + + main(client) { + + this.client = client; + if (client.check_on_init()) { + + this.amount = 42; + + // app-authored messages - the controller's MessageManager.addMessages + // equivalent. The z2ui5.cc.MessageManager companion reconciles this + // table into the central message manager: each row becomes a + // sap.ui.core.message.Message with the view's model as processor, so a + // row with a target sets that field's valueState too. + this.t_messages = [ + { message: "Please enter a valid name", + type: "Error", + additionaltext: "Name", + target: "/NAME" }, + { message: "Draft saved automatically", + type: "Information", + additionaltext: "Autosave" } ]; + + this.view_display(); + } else if (client.check_on_navigated()) { + this.view_display(); + + } + + } + + 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:z2ui5", v: "z2ui5.cc" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Message - Message Model and MessageManager" }) + .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: "Both sources of the central message> model in one page: the Name messages " + + "are AUTHORED BY THE APP (pushed from an ABAP table by the invisible " + + "z2ui5.cc.MessageManager companion - the Error targets the Name field and " + + "colours it), while typing letters into the Amount field collects the failed " + + "Integer validation AUTOMATICALLY - no app code, no roundtrip. Both render " + + "in the list below." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + // invisible companion control: reconciles /T_MESSAGES into the message + // manager (adds the app's messages, removes its own when they drop out, + // leaves auto-collected validation untouched) + page.ele({ n: "MessageManager", ns: "z2ui5" }) + .a({ n: "items", v: this.client._bind("t_messages") }); + + page.ele("VBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("Label") + .a({ n: "text", v: "Name (message authored by the app)" }) + .tag("Input") + .a({ n: "value", v: this.client._bind("name") }) + .tag("Label") + .a({ n: "text", v: "Amount (integer only - validation collected automatically)" }) + .tag("Input") + .a({ n: "value", v: `{ path: '${this.client._bind({ val: "amount", path: true })}', ` + + `type: 'sap.ui.model.type.Integer' }` }) + .a({ n: "width", v: "12rem" }); + + page.ele("List") + .a({ n: "headerText", v: "Collected messages (message> model)" }) + .a({ n: "items", v: "{message>/}" }) + .a({ n: "class", v: "sapUiSmallMargin" }) + .a({ n: "noDataText", v: "no messages" }) + .tag("StandardListItem") + .a({ n: "title", v: "{message>message}" }) + .a({ n: "description", v: "{message>additionalText}" }) + .a({ n: "info", v: "{message>type}" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_469.js b/srv/apps/z2ui5_cl_smp_app_469.js new file mode 100644 index 0000000..b1e3c4d --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_469.js @@ -0,0 +1,61 @@ +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_469.clas.abap +// Detail page shared by the two routing-mode demos - z2ui5_cl_smp_app_468 +// (mode fresh) and z2ui5_cl_smp_app_480 (mode keep). It is reached via +// client->nav_app_call( ), which - when hash routing is on - pushes a new route +// history entry, so the browser Back button returns to the hub it came from. +// This app only shows what to do next; it is a hidden helper (never listed on +// its own in the overview). +// +// In mode keep the browser Forward button (or a reload/bookmark of the route) +// restores this app from its draft and enters main( ) via +// check_on_navigated( ) - the view must be rendered again there, or the +// browser keeps showing the hub it navigated away from. +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_469", class { + + main(client) { + + this.client = client; + + // also covers the first start, plus browser Forward / reload / a bookmark + // restoring this app from its draft (routing mode keep) - the state + // survived, only the view must be rendered again + if (client.check_on_navigated()) { + this.view_display(); + } + + } + + 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" }); + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Navigation - Detail Page" }) + .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: "You navigated here from a routing-mode hub via nav_app_call. Now press your " + + "BROWSER Back button and watch the hub: mode keep restores its state, mode fresh " + + "restarts it empty." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.tag("Button") + .a({ n: "press", v: this.client._event_nav_app_leave() }) + .a({ n: "text", v: "back (in-app)" }) + .a({ n: "icon", v: "sap-icon://nav-back" }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_470.js b/srv/apps/z2ui5_cl_smp_app_470.js new file mode 100644 index 0000000..577c4ea --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_470.js @@ -0,0 +1,174 @@ +// @keywords element binding relative path aggregation dialog row +// @summary Element binding on a popup: the dialog binds RELATIVELY to the row that was pressed, so its fields need no path of their own. +// @docs https://abap2ui5.github.io/docs/cookbook/event_navigation/frontend https://abap2ui5.github.io/docs/cookbook/popup_popover/popup +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_470.clas.abap +// +// Aggregation binding on a popup via an element bind. The main table is bound +// to the product aggregation ({/T_PRODUCT}); pressing a row's "components" +// button opens a popup that uses RELATIVE bindings only ({NAME}, {CATEGORY}, and +// the nested aggregation {T_ITEM}). Instead of copying the row's data into event +// args, the app element-binds the whole popup slot to /T_PRODUCT/[index] with +// client->follow_up_action( cs_event-bind_element, view = cs_view-popup ), so the +// popup's relative bindings - including its component list's aggregation binding - +// resolve against the selected product. The row index arrives as the event arg +// (the pressed control's binding-context path). +import { defineApp, t, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +const ty_s_item = { + name: "", + qty: 0, + unit: "", +}; + +const ty_s_product = { + name: "", + category: "", + price: "", + t_item: t.table(ty_s_item), +}; + +defineApp("Z2UI5_CL_SMP_APP_470", class { + + t_product = t.table(ty_s_product); + + main(client) { + + this.client = client; + + if (client.check_on_init()) { + this.t_product = [ + { name: "Notebook 15\"", category: "Hardware", price: "1299", + t_item: [ { name: "SSD 1 TB", qty: 1, unit: "pc" }, + { name: "RAM 16 GB", qty: 2, unit: "pc" }, + { name: "Charger 90W", qty: 1, unit: "pc" } ] }, + { name: "Wireless Mouse", category: "Accessories", price: "39", + t_item: [ { name: "AA Battery", qty: 2, unit: "pc" } ] }, + { name: "USB-C Dock", category: "Accessories", price: "189", + t_item: [ { name: "Power Supply", qty: 1, unit: "pc" }, + { name: "Cable 1 m", qty: 1, unit: "pc" }, + { name: "Quick Guide", qty: 1, unit: "pc" } ] } ]; + this.view_display(); + } else if (client.check_on_navigated()) { + this.view_display(); + + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + on_event() { + + if (this.client.get_event() === "SHOW") { + this.popup_components(this.client.get_event_arg()); + } + + } + + 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" }); + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Popup - Element Binding to the Selected Row" }) + .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: "The table is bound to the product aggregation. Press a row's \"components\" button - the " + + "popup is element-bound to that product, so its relative bindings (incl. the component " + + "list's aggregation binding) resolve without copying any data into event args." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + const tab = page.ele("Table") + .a({ n: "items", v: this.client._bind("t_product") }); + + tab.ele("columns") + .ele("Column") + .tag("Text") + .a({ n: "text", v: "Product" }) + .end() + .ele("Column") + .tag("Text") + .a({ n: "text", v: "Category" }) + .end() + .ele("Column") + .tag("Text") + .a({ n: "text", v: "Price" }) + .end() + .ele("Column") + .tag("Text") + .a({ n: "text", v: "Components" }); + + tab.ele("items") + .ele("ColumnListItem") + .ele("cells") + .tag("Text") + .a({ n: "text", v: "{NAME}" }) + .tag("Text") + .a({ n: "text", v: "{CATEGORY}" }) + .tag("Text") + .a({ n: "text", v: "{PRICE} EUR" }) + .tag("Button") + .a({ n: "press", v: this.client._event({ + val: "SHOW", + arg: "$event.oSource.getBindingContext().getPath().split('/').pop()" }) }) + .a({ n: "text", v: "components" }) + .a({ n: "icon", v: "sap-icon://product" }); + + this.client.view_display(view.stringify()); + + } + + popup_components(index) { + + const 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" }); + const dialog = popup.ele("Dialog") + .a({ n: "title", v: "{NAME}" }) + .a({ n: "contentWidth", v: "24rem" }); + + // relative bindings - resolved by the element bind below + const box = dialog.ele("VBox") + .a({ n: "class", v: "sapUiSmallMarginBegin sapUiSmallMarginTop" }); + box.ele("ObjectStatus") + .a({ n: "text", v: "{CATEGORY}" }) + .a({ n: "title", v: "Category" }); + box.ele("ObjectStatus") + .a({ n: "text", v: "{PRICE} EUR" }) + .a({ n: "title", v: "Price" }); + + // the popup's own aggregation binding: the product's component list ({T_ITEM}) + dialog.ele("List") + .a({ n: "headerText", v: "Components" }) + .a({ n: "items", v: "{T_ITEM}" }) + .tag("StandardListItem") + .a({ n: "title", v: "{NAME}" }) + .a({ n: "info", v: "{QTY} {UNIT}" }); + + dialog.ele("buttons") + .tag("Button") + .a({ n: "press", v: this.client.follow_up_action(this.client.cs_event.popup_close) }) + .a({ n: "text", v: "Close" }) + .a({ n: "type", v: "Emphasized" }); + + this.client.popup_display(popup.stringify()); + + // element-bind the whole popup slot to the selected product, so every relative + // binding in the popup (and its component list aggregation) resolves against it + this.client.follow_up_action({ + val: this.client.cs_event.bind_element, + view: this.client.cs_view.popup, + t_arg: [ index, this.client._bind("t_product") ] }); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_472.js b/srv/apps/z2ui5_cl_smp_app_472.js new file mode 100644 index 0000000..809c282 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_472.js @@ -0,0 +1,101 @@ +// @keywords link href default action check_prevent_default +// @summary A Link whose default browser action is suppressed (check_prevent_default), so the app handles the click instead of the href. +// @docs https://abap2ui5.github.io/docs/cookbook/event_navigation/frontend +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_472.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_472", class { + + block_navigation = false; + last_press = ""; + + main(client) { + + this.client = client; + if (client.check_on_init()) { + this.block_navigation = true; + this.view_display(); + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + on_event() { + + switch (this.client.get_event()) { + + case "LINK_PRESS": + + if (this.block_navigation === true) { + this.last_press = "Link pressed - the browser did NOT follow the href, the backend decides what happens."; + } else { + this.last_press = "Link pressed - the href was followed by the browser as usual."; + } + break; + + case "TOGGLE": + // the flag is part of the event registration, so the view has to be + // rebuilt for the change to reach the frontend + this.view_display(); + break; + + } + + } + + 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:form", v: "sap.ui.layout.form" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Event - Link with preventDefault" }) + .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 sap.m.Link normally follows its href when pressed. Registered with " + + "s_ctrl-check_prevent_default the event cancels that built-in default " + + "(oEvent.preventDefault()) before the roundtrip - the event still reaches the " + + "backend, so the app decides what happens instead. Flip the switch to compare." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + const form = page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Link with a cancelled default" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }); + + form.tag("Label") + .a({ n: "text", v: "Cancel the browser navigation" }) + .tag("Switch") + .a({ n: "state", v: this.client._bind("block_navigation") }) + .a({ n: "change", v: this.client._event("TOGGLE") }) + .tag("Label") + .a({ n: "text", v: "Link" }) + .tag("Link") + .a({ n: "text", v: "Open abap2ui5.org" }) + .a({ n: "target", v: "_blank" }) + .a({ n: "href", v: "https://abap2ui5.org" }) + .a({ n: "press", v: this.client._event({ + val: "LINK_PRESS", + s_ctrl: { check_prevent_default: this.block_navigation } }) }) + .tag("Label") + .a({ n: "text", v: "Result" }) + .tag("Text") + .a({ n: "text", v: this.client._bind("last_press") }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_473.js b/srv/apps/z2ui5_cl_smp_app_473.js new file mode 100644 index 0000000..af2a003 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_473.js @@ -0,0 +1,81 @@ +// @keywords menuitem nested submenu textpath controller path +// @summary A nested Menu that reports the FULL path of the item that was chosen, not just its text. +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_473.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_473", class { + + main(client) { + + this.client = client; + if (client.check_on_navigated()) { + this.view_display(); + } + + } + + 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" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Menu - Full Path of the Selected Item" }) + .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: "The toast shows the full path of the selected menu item " + + "(\"Create New Site > Official Store\"), not only its own text. " + + "$controller.textPath( ) walks the item's parent chain in the control tree " + + "and joins the texts - a walk no binding path can express. Everything happens " + + "on the client, the menu selection needs no roundtrip at all." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + // the item's breadcrumb is resolved on the client and substituted into the + // toast template ({0}); an argument starting with $ is a client-side + // expression, everything else travels as a plain string + const menu_selected = this.client.follow_up_action({ + val: z2ui5_if_client.cs_event.control_global, + t_arg: [ "MESSAGE_TOAST", + "show", + "Action triggered on item: {0}", + "$controller.textPath(${$parameters>/item})" ] }); + + const menu = page.ele("HBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .ele("MenuButton") + .a({ n: "text", v: "Actions" }) + .ele("Menu") + .a({ n: "itemSelected", v: menu_selected }); + + menu.ele("MenuItem") + .a({ n: "text", v: "Create New Site" }) + .tag("MenuItem") + .a({ n: "text", v: "Official Store" }) + .tag("MenuItem") + .a({ n: "text", v: "Franchise Store" }) + .tag("MenuItem") + .a({ n: "text", v: "Pop-up Store" }); + + menu.ele("MenuItem") + .a({ n: "text", v: "Manage Users" }) + .tag("MenuItem") + .a({ n: "text", v: "Add User" }) + .tag("MenuItem") + .a({ n: "text", v: "Remove User" }); + + menu.tag("MenuItem") + .a({ n: "text", v: "Log Out" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_474.js b/srv/apps/z2ui5_cl_smp_app_474.js new file mode 100644 index 0000000..50d82ce --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_474.js @@ -0,0 +1,132 @@ +// @keywords url policy link security validator relative allow deny +// @summary The URL policy of a MessagePopover: which links it will follow and which it refuses, and why the default is the strict one. +// @docs https://abap2ui5.github.io/docs/cookbook/translation_messages/message +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_474.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_474", class { + + main(client) { + + this.client = client; + if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + on_event() { + + switch (this.client.get_event()) { + + case "OPEN_RELATIVE_ONLY": + this.popover_open("RELATIVE_ONLY"); + break; + + case "OPEN_ALLOW_ALL": + this.popover_open("ALLOW_ALL"); + break; + + case "OPEN_DENY_ALL": + this.popover_open("DENY_ALL"); + break; + + } + + } + + popover_open(policy) { + + // setAsyncURLHandler takes a JS callback in a UI5 controller, which no + // backend payload can carry - the frontend therefore takes the NAME of a + // built-in policy and installs the matching validator itself: + // RELATIVE_ONLY (only in-app links stay clickable), ALLOW_ALL, DENY_ALL + // + // The handler is live control state a rebuild destroys, and this method is + // not on the display path - control-state-lost-on-rebuild is right about + // both. It is still not a defect HERE: every open goes through this method, + // so the validator is re-installed immediately before each openBy( ) below, + // and there is no window in which the popover is visible without it. That + // is the one thing the rule cannot see, so it is said here instead. + // re-issued before every openBy + this.client.follow_up_action({ val: z2ui5_if_client.cs_event.control_by_id, + t_arg: [ "msgPopover", + "setAsyncURLHandler", + policy ] }); + + // ... and only then open it, anchored to the button that fired the event + this.client.follow_up_action({ val: z2ui5_if_client.cs_event.control_by_id, + t_arg: [ "msgPopover", + "openBy", + this.client.get_event_arg() ] }); + + } + + 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" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Message - MessagePopover URL Policy" }) + .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: "Each message below carries an in-app link and an external one. The policy " + + "applied when opening decides which of them the popover keeps clickable - " + + "RELATIVE_ONLY blocks everything that leaves the app, ALLOW_ALL keeps every " + + "link, DENY_ALL blocks all of them. The policy travels as data, the frontend " + + "installs the validator (setAsyncURLHandler)." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele("dependents") + .ele("MessagePopover") + .a({ n: "id", v: "msgPopover" }) + .ele("MessageItem") + .a({ n: "type", v: "Error" }) + .a({ n: "title", v: "Order cannot be released" }) + .a({ n: "description", v: "Check the order details or the " + + "documentation." }) + .a({ n: "markupDescription", b: true }) + .end() + .ele("MessageItem") + .a({ n: "type", v: "Warning" }) + .a({ n: "title", v: "Delivery date in the past" }) + .a({ n: "description", v: "Open the delivery list or the " + + "UI5 demo kit." }) + .a({ n: "markupDescription", b: true }) + .end() + .end(); + + page.ele("HBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("Button") + .a({ n: "press", v: this.client._event({ val: "OPEN_RELATIVE_ONLY", + arg: "$event.oSource.sId" }) }) + .a({ n: "text", v: "Open with RELATIVE_ONLY" }) + .a({ n: "type", v: "Emphasized" }) + .tag("Button") + .a({ n: "press", v: this.client._event({ val: "OPEN_ALLOW_ALL", + arg: "$event.oSource.sId" }) }) + .a({ n: "text", v: "Open with ALLOW_ALL" }) + .a({ n: "class", v: "sapUiTinyMarginBegin" }) + .tag("Button") + .a({ n: "press", v: this.client._event({ val: "OPEN_DENY_ALL", + arg: "$event.oSource.sId" }) }) + .a({ n: "text", v: "Open with DENY_ALL" }) + .a({ n: "class", v: "sapUiTinyMarginBegin" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_488.js b/srv/apps/z2ui5_cl_smp_app_488.js index 91655a7..711eb57 100644 --- a/srv/apps/z2ui5_cl_smp_app_488.js +++ b/srv/apps/z2ui5_cl_smp_app_488.js @@ -3,17 +3,20 @@ // @docs https://abap2ui5.github.io/docs/cookbook/event_navigation/navigation/inner_app // @origin abap2UI5/samples src/z2ui5_cl_smp_app_488.clas.abap // -// Calls a second app (z2ui5_cl_smp_app_489) via client.nav_app_call( ). The -// called app comes back with client.nav_app_leave( { event, r_data } ), +// Calls a second app (z2ui5_cl_smp_app_489) via client->nav_app_call( ). The +// called app comes back with client->nav_app_leave( event = ... r_data = ... ), // handing an event name and a data payload to its caller without knowing who // called it. On return this app enters main( ) via check_on_navigated( ) and -// reads both from client.get( ): the event name from .event, the payload from -// .r_event_data. +// reads both from client->get( ): the event name from -event, the payload as a +// data reference from -r_event_data. import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; defineApp("Z2UI5_CL_SMP_APP_488", class { - s_result = { product: "", quantity: "" }; // z2ui5_cl_smp_app_489=>ty_s_result + s_result = { // z2ui5_cl_smp_app_489=>ty_s_result + product: "", + quantity: "", + }; returned_event = ""; main(client) { @@ -33,17 +36,18 @@ defineApp("Z2UI5_CL_SMP_APP_488", class { } on_navigation() { + let s_result; const ls_get = this.client.get(); this.returned_event = ls_get.event; switch (this.returned_event) { - case "DATA_CONFIRMED": { + case "DATA_CONFIRMED": - // the payload handed over by nav_app_leave( r_data = ... ) arrives as - // plain data - the receiver decides what it is - const s_result = ls_get.r_event_data; + // the payload handed over by nav_app_leave( r_data = ... ) arrives as a + // generic data reference - the receiver decides the type + s_result = ls_get.r_event_data; if (s_result) { @@ -53,7 +57,6 @@ defineApp("Z2UI5_CL_SMP_APP_488", class { } break; - } case "DATA_CANCELLED": @@ -79,14 +82,14 @@ defineApp("Z2UI5_CL_SMP_APP_488", class { .a({ n: "xmlns:layout", v: "sap.ui.layout" }); const page = view.ele("Shell") .ele("Page") - .a({ n: "title", v: "cap2UI5 - Navigation - Return Data and Events to the Caller" }) + .a({ n: "title", v: "abap2UI5 - Navigation - Return Data and Events to the Caller" }) .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: "Calls a second app that returns via nav_app_leave with an event and a data " + - "payload (r_data). On return this app reads both from client.get( ) in its " + - "check_on_navigated( ) branch and shows them below." }) + "payload (r_data). On return this app reads both from client->get( ) in its " + + "check_on_navigated( ) branch and shows them below." }) .a({ n: "type", v: "Information" }) .a({ n: "showIcon", b: true }) .a({ n: "class", v: "sapUiSmallMargin" }); @@ -127,5 +130,4 @@ defineApp("Z2UI5_CL_SMP_APP_488", class { this.client.view_display(view.stringify()); } - }); diff --git a/srv/apps/z2ui5_cl_smp_app_489.js b/srv/apps/z2ui5_cl_smp_app_489.js index 8c50400..143d7ab 100644 --- a/srv/apps/z2ui5_cl_smp_app_489.js +++ b/srv/apps/z2ui5_cl_smp_app_489.js @@ -1,15 +1,19 @@ // @origin abap2UI5/samples src/z2ui5_cl_smp_app_489.clas.abap -// // Input page called by z2ui5_cl_smp_app_488. It returns to its caller with -// client.nav_app_leave( { event, r_data } ) - handing back an event name and -// the entered data without knowing which app called it (no get_app_prev( ), -// no look at the caller's fields). This app is a hidden helper (never listed -// on its own in the overview). +// client->nav_app_leave( event = ... r_data = ... ) - handing back an event +// name and the entered data without knowing which app called it (no +// get_app_prev( ), no cast to the caller's class). This app is a hidden +// helper (never listed on its own in the overview). import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; +const ty_s_result = { + product: "", + quantity: "", +}; + defineApp("Z2UI5_CL_SMP_APP_489", class { - s_result = { product: "", quantity: "" }; // ty_s_result + s_result = ty_s_result; main(client) { @@ -45,14 +49,14 @@ defineApp("Z2UI5_CL_SMP_APP_489", class { .a({ n: "xmlns:layout", v: "sap.ui.layout" }); const page = view.ele("Shell") .ele("Page") - .a({ n: "title", v: "cap2UI5 - Navigation - Data Input App" }) + .a({ n: "title", v: "abap2UI5 - Navigation - Data Input App" }) .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: "Change the data and return: 'confirm' leaves with event DATA_CONFIRMED plus the " + - "entered data as r_data, 'cancel' leaves with event DATA_CANCELLED and no data. " + - "The nav-back button of the page leaves without an event." }) + "entered data as r_data, 'cancel' leaves with event DATA_CANCELLED and no data. " + + "The nav-back button of the page leaves without an event." }) .a({ n: "type", v: "Information" }) .a({ n: "showIcon", b: true }) .a({ n: "class", v: "sapUiSmallMargin" }); @@ -88,5 +92,4 @@ defineApp("Z2UI5_CL_SMP_APP_489", class { this.client.view_display(view.stringify()); } - }); diff --git a/srv/apps/z2ui5_cl_smp_app_490.js b/srv/apps/z2ui5_cl_smp_app_490.js new file mode 100644 index 0000000..a940f25 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_490.js @@ -0,0 +1,104 @@ +// @keywords initial render one roundtrip anchor button +// @summary View and popover in ONE roundtrip: both displayed from the same main( ) call, with the popover anchored to a button built in that very response. +// @docs https://abap2ui5.github.io/docs/cookbook/popup_popover/popover +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_490.clas.abap +// +// View and popover in ONE roundtrip: view_display( ) and popover_display( ) +// from the same main( ) call. The popover anchors to a button of the view +// that is built in this very response - the framework runs the display +// actions in order and awaits each build, so the anchor exists by the time +// the popover opens. Also re-opens the popover TOGETHER with a view rebuild +// on an event, and contrasts it with a popover alone (no view rebuild). +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_490", class { + + counter = 0; + + main(client) { + + this.client = client; + + if (client.check_on_init()) { + // the initial response already carries BOTH: the view build and the + // popover anchored to a control of that very view + this.view_display(); + this.popover_display(); + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event("REBUILD_AND_OPEN")) { + // same pair on an event roundtrip: the view is REPLACED and the + // popover opens on the freshly built anchor + this.counter = this.counter + 1; + this.view_display(); + this.popover_display(); + } else if (client.check_on_event("OPEN_ONLY")) { + // popover alone - the view stays as it is + this.counter = this.counter + 1; + this.popover_display(); + } + + } + + 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" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Popover - Open Together with the View Build" }) + .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: "This response built the page AND opened the popover in one roundtrip - " + + "the popover anchors to the button below, which only exists once this " + + "view is rendered. Both buttons re-open it: the first rebuilds the view " + + "with it, the second opens it alone." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele("VBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("Button") + .a({ n: "press", v: this.client._event("REBUILD_AND_OPEN") }) + .a({ n: "text", v: "rebuild view + open popover" }) + .a({ n: "icon", v: "sap-icon://refresh" }) + .a({ n: "id", v: "btnAnchor" }) + .tag("Button") + .a({ n: "press", v: this.client._event("OPEN_ONLY") }) + .a({ n: "text", v: "open popover only (view untouched)" }); + + this.client.view_display(view.stringify()); + + } + + popover_display() { + + const popover = 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" }); + popover.ele("Popover") + .a({ n: "title", v: "Opened with the view" }) + .a({ n: "placement", v: "Bottom" }) + .a({ n: "contentWidth", v: "20rem" }) + .ele("VBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("Text") + .a({ n: "text", v: "This popover travelled in the SAME response as the view it is " + + "anchored to." }) + .ele("ObjectStatus") + .a({ n: "state", v: "Information" }) + .a({ n: "text", t: `roundtrips so far: ${this.counter}` }); + + this.client.popover_display({ xml: popover.stringify(), by_id: "btnAnchor" }); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_491.js b/srv/apps/z2ui5_cl_smp_app_491.js new file mode 100644 index 0000000..4feda9a --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_491.js @@ -0,0 +1,57 @@ +// @keywords favicon icon tab image data uri +// @summary Sets the browser tab's favicon at runtime, from an image the backend hands over as a data URI. +// @docs https://abap2ui5.github.io/docs/cookbook/browser_interaction/title +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_491.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_491", class { + + favicon = "data:image/svg+xml,"; + + main(client) { + + if (client.check_on_navigated()) { + + 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:form", v: "sap.ui.layout.form" }); + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Browser - Set the Tab Favicon" }) + .a({ n: "showNavButton", b: client.check_app_prev_stack() }) + .a({ n: "navButtonPress", v: client._event_nav_app_leave() }); + + page.tag("MessageStrip") + .a({ n: "text", v: "Enter an image URL (or data URI) and press the button to run the set_favicon front-end action, " + + "which updates the browser tab icon (the link rel=\"icon\" tag) without reloading the page." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Favicon" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }) + .tag("Label") + .a({ n: "text", v: "favicon url" }) + .tag("Input") + .a({ n: "value", v: client._bind("favicon") }) + .tag("Button") + .a({ n: "press", v: client._event("SET_FAVICON") }) + .a({ n: "text", v: "Set Favicon" }); + client.view_display(view.stringify()); + + } else if (client.check_on_event("SET_FAVICON")) { + + client.follow_up_action({ + val: z2ui5_if_client.cs_event.set_favicon, + t_arg: [ this.favicon ] }); + + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_492.js b/srv/apps/z2ui5_cl_smp_app_492.js new file mode 100644 index 0000000..3d545b1 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_492.js @@ -0,0 +1,67 @@ +// @keywords reload refresh restart location_reload url +// @summary Navigates the browser to a same-domain URL with the location_reload front-end action, with a scratch input beside it to show what the reload takes with it. +// @docs https://abap2ui5.github.io/docs/cookbook/browser_interaction/url_handling +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_492.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_492", class { + + url = ""; + scratch = ""; + + main(client) { + + if (client.check_on_navigated()) { + + const s_config = client.get().s_config; + this.url = s_config.pathname + s_config.search; + + 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:form", v: "sap.ui.layout.form" }); + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Browser - Reload the Page" }) + .a({ n: "showNavButton", b: client.check_app_prev_stack() }) + .a({ n: "navButtonPress", v: client._event_nav_app_leave() }); + + page.tag("MessageStrip") + .a({ n: "text", v: "The location_reload front-end action navigates the browser to a same-domain URL. The url field is " + + "prefilled with this page's own address, so the button reloads the app from scratch - type something " + + "into the scratch field first and watch it get lost. Cross-origin URLs are blocked by the framework." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Reload" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }) + .tag("Label") + .a({ n: "text", v: "scratch input" }) + .tag("Input") + .a({ n: "placeholder", v: "type something - it is lost after the reload" }) + .a({ n: "value", v: client._bind("scratch") }) + .tag("Label") + .a({ n: "text", v: "url" }) + .tag("Input") + .a({ n: "value", v: client._bind("url") }) + .tag("Button") + .a({ n: "press", v: client._event("RELOAD") }) + .a({ n: "text", v: "Reload Page" }); + client.view_display(view.stringify()); + + } else if (client.check_on_event("RELOAD")) { + + client.follow_up_action({ + val: z2ui5_if_client.cs_event.location_reload, + t_arg: [ this.url ] }); + + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_493.js b/srv/apps/z2ui5_cl_smp_app_493.js index bc7732e..33bd333 100644 --- a/srv/apps/z2ui5_cl_smp_app_493.js +++ b/srv/apps/z2ui5_cl_smp_app_493.js @@ -18,16 +18,16 @@ defineApp("Z2UI5_CL_SMP_APP_493", class { .a({ n: "xmlns:mvc", v: "sap.ui.core.mvc" }); const page = view.ele("Shell") .ele("Page") - .a({ n: "title", v: "cap2UI5 - Basics I - Hello World, the Smallest App" }) + .a({ n: "title", v: "abap2UI5 - Basics I - Hello World, the Smallest App" }) .a({ n: "showNavButton", b: client.check_app_prev_stack() }) .a({ n: "navButtonPress", v: client._event_nav_app_leave() }); page.tag("MessageStrip") - .a({ n: "text", v: "The whole app is what you see below: a class handed to defineApp( ), " + - "one main( ) method, a view built as XML and handed to client.view_display( ). " + - "cap2UI5 calls main( ) on every roundtrip - here only the display matters, " + - "which is what check_on_navigated( ) asks: true on the first start and whenever " + - "the app is shown again. Copy this file as the starting point for your own app." }) + .a({ n: "text", v: "The whole app is what you see below: a class implementing z2ui5_if_app, " + + "one main( ) method, a view built as XML and handed to client->view_display( ). " + + "abap2UI5 calls main( ) on every roundtrip - here only the display matters, " + + "which is what check_on_navigated( ) asks: true on the first start and whenever " + + "the app is shown again. Copy this class as the starting point for your own app." }) .a({ n: "type", v: "Information" }) .a({ n: "showIcon", b: true }) .a({ n: "class", v: "sapUiSmallMargin" }); diff --git a/srv/apps/z2ui5_cl_smp_app_494.js b/srv/apps/z2ui5_cl_smp_app_494.js index 618898e..bea35c9 100644 --- a/srv/apps/z2ui5_cl_smp_app_494.js +++ b/srv/apps/z2ui5_cl_smp_app_494.js @@ -1,5 +1,5 @@ // @keywords binding _bind model attribute value input button roundtrip messagebox serialize -// @summary Binds a class attribute to an Input with _bind( ), so what the user types is in the variable on the next roundtrip - a Text shows it back and a MessageBox confirms the roundtrip. +// @summary Binds a class attribute to an Input with _bind( ), so what the user types is in the ABAP variable on the next roundtrip - a Text shows it back and a MessageBox confirms the roundtrip. // @docs https://abap2ui5.github.io/docs/cookbook/model/binding https://abap2ui5.github.io/docs/tutorials/walkthrough/step-4 // @origin abap2UI5/samples src/z2ui5_cl_smp_app_494.clas.abap import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; @@ -35,17 +35,17 @@ defineApp("Z2UI5_CL_SMP_APP_494", class { .a({ n: "xmlns:form", v: "sap.ui.layout.form" }); const page = view.ele("Shell") .ele("Page") - .a({ n: "title", v: "cap2UI5 - Basics II - Data Binding: Input and Button" }) + .a({ n: "title", v: "abap2UI5 - Basics II - Data Binding: Input and Button" }) .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: "client._bind( 'name' ) connects the field NAME with the input " + - "below. Type a name and leave the field: the text " + - "next to it changes without any backend code, because both are bound to the " + - "same field. Press Greet and the backend reads NAME - already filled " + - "in, no event argument needed -, writes GREETING back into the view and " + - "confirms the roundtrip with a MessageBox." }) + .a({ n: "text", v: "client->_bind( name ) connects the public attribute NAME with the input " + + "below. Type a name and leave the field: the text " + + "next to it changes without any ABAP code, because both are bound to the " + + "same attribute. Press Greet and the backend reads NAME - already filled " + + "in, no event argument needed -, writes GREETING back into the view and " + + "confirms the roundtrip with a MessageBox." }) .a({ n: "type", v: "Information" }) .a({ n: "showIcon", b: true }) .a({ n: "class", v: "sapUiSmallMargin" }); @@ -59,7 +59,7 @@ defineApp("Z2UI5_CL_SMP_APP_494", class { .tag("Input") .a({ n: "value", v: this.client._bind("name") }) .tag("Label") - .a({ n: "text", v: "bound to the same field" }) + .a({ n: "text", v: "bound to the same attribute" }) .tag("Text") .a({ n: "text", v: this.client._bind("name") }) .tag("Label") diff --git a/srv/apps/z2ui5_cl_smp_app_495.js b/srv/apps/z2ui5_cl_smp_app_495.js index 038d54d..1d04751 100644 --- a/srv/apps/z2ui5_cl_smp_app_495.js +++ b/srv/apps/z2ui5_cl_smp_app_495.js @@ -4,9 +4,14 @@ // @origin abap2UI5/samples src/z2ui5_cl_smp_app_495.clas.abap import { defineApp, t, z2ui5_cl_ui5_view_builder } from "cap2ui5"; +const ty_s_step = { + no: "", + check: "", +}; + defineApp("Z2UI5_CL_SMP_APP_495", class { - t_log = t.table({ no: "", check: "" }); + t_log = t.table(ty_s_step); main(client) { @@ -35,8 +40,7 @@ defineApp("Z2UI5_CL_SMP_APP_495", class { log_step(val) { - // a read of t_log is a copy: the table with the new row is written back whole - this.t_log = [...this.t_log, { no: String(this.t_log.length + 1), check: val }]; + this.t_log = [...this.t_log, { no: `${this.t_log.length + 1}`, check: val }]; } @@ -50,17 +54,17 @@ defineApp("Z2UI5_CL_SMP_APP_495", class { .a({ n: "xmlns:mvc", v: "sap.ui.core.mvc" }); const page = view.ele("Shell") .ele("Page") - .a({ n: "title", v: "cap2UI5 - Basics III - Lifecycle: Init, Event, Navigated" }) + .a({ n: "title", v: "abap2UI5 - Basics III - Lifecycle: Init, Event, Navigated" }) .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: "main( ) runs on every roundtrip - the three checks tell it what the " + - "roundtrip is about. The list logs each call, and it survives them all: " + - "every field is serialized between the roundtrips, so the app " + - "keeps its state without a table of its own. Press Log - only the model is pushed, " + - "the view is not rebuilt. Call the sub-app and come back with its back " + - "button - that is the roundtrip check_on_navigated( ) answers." }) + "roundtrip is about. The list logs each call, and it survives them all: " + + "every public attribute is serialized between the roundtrips, so the app " + + "keeps its state without a database. Press Log - only the model is pushed, " + + "the view is not rebuilt. Call the sub-app and come back with its back " + + "button - that is the roundtrip check_on_navigated( ) answers." }) .a({ n: "type", v: "Information" }) .a({ n: "showIcon", b: true }) .a({ n: "class", v: "sapUiSmallMargin" }); diff --git a/srv/apps/z2ui5_cl_smp_app_496.js b/srv/apps/z2ui5_cl_smp_app_496.js new file mode 100644 index 0000000..c3958a9 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_496.js @@ -0,0 +1,140 @@ +// @keywords developer tools devtools ctrl f12 debug inspect payload previous request response view xml view model source code log error adt export +// @summary What Ctrl+F12 opens: the request and response payload, the generated XML view, the model, the source and the error log - the first place to look when something does not render. +// @docs https://abap2ui5.github.io/docs/cookbook/troubleshooting/common_failures +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_496.clas.abap +import { defineApp, t, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +const ty_s_tab = { + name: "", + descr: "", +}; + +const cs_event = { + ping: "PING", + where: "WHERE", +}; + +defineApp("Z2UI5_CL_SMP_APP_496", class { + + // a public attribute is serialized between the roundtrips - so everything + // on this page is part of what the View Model tab of the tools shows + t_tab = t.table(ty_s_tab); + text = ""; + roundtrips = 0; + + main(client) { + + this.client = client; + + if (client.check_on_init()) { + + this.text = "change me and press Send"; + this.tabs_init(); + this.view_display(); + } else if (client.check_on_navigated()) { + this.view_display(); + + } else if (client.check_on_event(cs_event.ping)) { + + // nothing to compute - the point is the roundtrip itself, so there is + // something to look at in Previous Request and Response + this.roundtrips = this.roundtrips + 1; + + } else if (client.check_on_event(cs_event.where)) { + + // there is no ABAP call that opens the tools: they are a control of the + // frontend, toggled by the shortcut - the framework's own start page + // answers this question the same way + client.message_box_display({ + title: "Developer Tools", + text: "Press Ctrl+F12 to open and close the developer tools. They belong to the framework, " + + "not to this sample - the shortcut works in every abap2UI5 app." }); + + } + + } + + tabs_init() { + + this.t_tab = [ + { name: "Error", + descr: "The last uncaught exception with its call stack - the same one the error popup shows." }, + { name: "Log", + descr: "What the frontend did since the app started: roundtrips, frontend actions, events." }, + { name: "Previous Request", + descr: "The JSON this browser sent last - your event, the changed model, the app state." }, + { name: "Response", + descr: "The JSON the backend sent back - the new view, the new model, the follow-up actions." }, + { name: "Source Code", + descr: "The ABAP class behind the running app, with an ADT jump link in the dialog footer." }, + { name: "View", + descr: "The XML view your ABAP built - what z2ui5_cl_ui5_view_builder stringified into the response." }, + { name: "View Model", + descr: "The model behind that view: every bound attribute of this class with its live value." }, + { name: "Popup, Popover, Nest1, Nest2", + descr: "The same two tabs - view and model - for each of the other view slots of the app." } ]; + + } + + 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:form", v: "sap.ui.layout.form" }); + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Basics V - The Developer Tools (Ctrl+F12)" }) + .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: "Press Ctrl+F12 - here and in every other abap2UI5 app - and the developer tools " + + "open over the app. They show what travels between this class and the browser: the " + + "XML view your ABAP built, the model behind it, and the JSON of the last request and " + + "response. Change the text below, press Send, and look at Previous Request: the value " + + "you typed is in it. Then look at View Model - it is there too, because a public " + + "attribute is the model." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Something to look at" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }) + .tag("Label") + .a({ n: "text", v: "bound to the public attribute TEXT" }) + .tag("Input") + .a({ n: "value", v: this.client._bind("text") }) + .tag("Label") + .a({ n: "text", v: "roundtrips so far" }) + .tag("Text") + .a({ n: "text", v: this.client._bind("roundtrips") }) + .tag("Label") + .a({ n: "text", v: "send it to the backend" }) + .tag("Button") + .a({ n: "press", v: this.client._event(cs_event.ping) }) + .a({ n: "text", v: "Send" }) + .tag("Label") + .a({ n: "text", v: "how do I open the tools?" }) + .tag("Button") + .a({ n: "press", v: this.client._event(cs_event.where) }) + .a({ n: "text", v: "Show me" }) + .a({ n: "icon", v: "sap-icon://sys-help" }); + + page.ele("List") + .a({ n: "headerText", v: "What the tabs of the tools show" }) + .a({ n: "items", v: this.client._bind("t_tab") }) + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("StandardListItem") + .a({ n: "title", v: "{NAME}" }) + .a({ n: "description", v: "{DESCR}" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_498.js b/srv/apps/z2ui5_cl_smp_app_498.js new file mode 100644 index 0000000..9433da1 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_498.js @@ -0,0 +1,165 @@ +// @keywords app state url bookmark share clipboard copy link restore deep link reload app_state_set_active app_state_get_href sap-iapp-state sap-xapp-state switch off event form +// @summary The Fiori app-state pattern: the URL carries the state id, so a bookmark, a reload or a shared link restores the entered data. +// @docs https://abap2ui5.github.io/docs/cookbook/event_navigation/navigation/app_state +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_498.clas.abap +// +// The abap2UI5 spelling of the Fiori app state (sap-iapp-state / +// sap-xapp-state): the app state IS the draft the framework persists anyway, +// so app_state_set_active( ) only has to carry its id in the URL +// ('#/z2ui5-xapp-state=<id>'). From then on every roundtrip advances the id, +// so the address bar always names the CURRENT state: +// +// - reload or bookmark the page - the entered data comes back +// - press SHARE - app_state_get_href( ) hands the BACKEND the absolute +// link to exactly this state (FLP-safe: the shell hash survives in it), +// so the app shows it, copies it with the plain clipboard_copy action, +// and could just as well mail it or render it as a QR code +// +// The state lives until the draft expires; an expired link starts the app +// fresh and says so. The switch at the bottom turns the URL tracking off +// and on again through the EVENT form, follow_up_action with +// cs_event-app_state_set_active: an empty argument list switches it on, a +// single space is how the event form says false. Consolidates the former z2ui5_cl_smp_app_321 (bookmark) +// and z2ui5_cl_smp_app_323 (share) into one sample. +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_498", class { + + quantity = ""; + notes = ""; + share_link = ""; + state_in_url = false; + + main(client) { + + this.client = client; + + if (client.check_on_init()) { + // keep the current app state's id in the URL from the first render on - + // the framework re-asserts it on every response, so the address bar + // tracks every later roundtrip too. check_on_init implies + // check_on_navigated, so the first display happens right below + client.app_state_set_active(); + this.state_in_url = true; + this.view_display(); + + } else if (client.check_on_navigated()) { + this.view_display(); + + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + 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:form", v: "sap.ui.layout.form" }); + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Hash - App State, Bookmark and Share" }) + .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: "app_state_set_active( ) keeps the id of the CURRENT app state in the URL " + + "(#/z2ui5-xapp-state=...). Type something, press post - and then reload the page, " + + "bookmark it, or share it: the state comes back." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + const form = page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "The state a link can carry" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }); + + form.tag("Label") + .a({ n: "text", v: "quantity" }); + form.tag("Input") + .a({ n: "value", v: this.client._bind("quantity") }); + + form.tag("Label") + .a({ n: "text", v: "notes" }); + form.tag("Input") + .a({ n: "value", v: this.client._bind("notes") }); + + form.tag("Button") + .a({ n: "press", v: this.client._event("POST") }) + .a({ n: "text", v: "post" }) + .a({ n: "type", v: "Emphasized" }); + form.tag("Button") + .a({ n: "press", v: this.client._event("SHARE") }) + .a({ n: "text", v: "share - copy the link" }) + .a({ n: "icon", v: "sap-icon://chain-link" }); + + // the backend OWNS the link string (app_state_get_href), so the app can + // show it - something the old fire-and-forget clipboard action never could + form.tag("Label") + .a({ n: "text", v: "the link the share button copies" }); + form.tag("Input") + .a({ n: "value", v: this.client._bind("share_link") }) + .a({ n: "editable", b: false }); + + form.tag("Label") + .a({ n: "text", v: "keep the state id in the URL" }); + form.tag("Switch") + .a({ n: "state", v: this.client._bind("state_in_url") }) + .a({ n: "change", v: this.client._event("TOGGLE_STATE") }); + + page.tag("MessageStrip") + .a({ n: "text", v: "Share copies a link to exactly this state into the clipboard - the Fiori " + + "sap-xapp-state idea with the draft as the state container. The link lives until " + + "the draft expires; after that it starts the app fresh." }) + .a({ n: "type", v: "Success" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + this.client.view_display(view.stringify()); + + } + + on_event() { + + switch (this.client.get_event()) { + + case "POST": + // any roundtrip advances the draft - and with it the id in the URL + this.client.message_toast_display("data updated - the URL now names this state"); + break; + + case "TOGGLE_STATE": + // the event form of app_state_set_active( ): no argument switches + // the tracking on, a single space switches it off - an empty t_arg + // cannot say false, so that is the encoding the event form uses + if (this.state_in_url === true) { + this.client.follow_up_action(z2ui5_if_client.cs_event.app_state_set_active); + this.client.message_toast_display("the URL names the current state again"); + } else { + this.client.follow_up_action({ val: z2ui5_if_client.cs_event.app_state_set_active, + t_arg: [ " " ] }); + this.client.message_toast_display("switched off - the URL stops following the state"); + } + break; + + case "SHARE": + // the link to exactly THIS roundtrip's state, composed backend-side - + // origin, path and (inside the FLP) the shell hash all survive in it. + // Copying is then just the generic clipboard action; the same string + // could go into a mail or a QR code + this.share_link = this.client.app_state_get_href(); + this.client.follow_up_action({ val: z2ui5_if_client.cs_event.clipboard_copy, + t_arg: [ this.share_link ] }); + this.client.message_toast_display("link copied - open it anywhere"); + break; + + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_499.js b/srv/apps/z2ui5_cl_smp_app_499.js new file mode 100644 index 0000000..bf0c8c3 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_499.js @@ -0,0 +1,243 @@ +// @keywords routing hash url page browser back forward history deep link reload hash_set hash_replace hash_back hash_attach_changed navcontainer router onnavback follow_up_action event form +// @summary The whole hash_* family in one app: hash_set pushes #/detail, hash_replace rewrites it in place, hash_back steps back like a router, a deep link restores. +// @docs https://abap2ui5.github.io/docs/cookbook/event_navigation/navigation/hash +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_499.clas.abap +// +// App-owned hash routing - the URL semantics of a UI5 router, 1:1, and the +// whole hash_* family (named after sap/ui/core/routing/HashChanger) in one +// place: +// +// - the start URL carries NO hash +// - opening the detail page writes '#/detail' via hash_set - setHash, a +// real PUSHED history entry +// - switching the detail variant rewrites the URL via hash_replace - +// replaceHash, NO new entry: the browser Back button skips the variant +// switches and returns straight to the first page +// - the BROWSER Back/Forward buttons - and a manual edit of the hash - +// round-trip as the event hash_attach_changed registered, and the +// handler shows the page the live hash (get( )-s_config-hash) names; +// the app instance is untouched, so the entered data survives +// - the in-app back button is cs_event-hash_back with '/' as its +// FALLBACK: the UI5 onNavBack pattern. Normally one real, consumed +// window.history.go(-1) - but on a COLD deep link ('#/detail' opened +// fresh) there is no in-app step to take, so the fallback replaces to +// the start page instead of falling out of the app +// - a reload or a shared link with '#/detail' lands on the detail page +// - hash_set( ) and hash_replace( ) have an EVENT form, follow_up_action +// with cs_event-hash_set / cs_event-hash_replace: the same write, for an +// app that keeps its follow-ups in one shape. The start-page button on +// the detail page uses the first, the repair of an unknown variant in +// the hash the second +// +// The draft-based routing modes (cs_event-hash_routing) are the siblings +// z2ui5_cl_smp_app_468 and z2ui5_cl_smp_app_480. Replaces the former +// z2ui5_cl_smp_app_322, which pushed a suffix past the UI5 HashChanger and +// had to reach for raw JavaScript to step back. +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_499", class { + + quantity = ""; + variant = "a"; + + check_detail = false; + + main(client) { + + this.client = client; + + if (client.check_on_navigated()) { + this.view_display(); + + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + view_display() { + + // a deep link / reload: the live hash rides in s_config-hash on every + // request, so a render whose hash already names the detail page starts + // there - the routeMatched of a cold start + this.hash_apply(); + + 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:form", v: "sap.ui.layout.form" }); + const nav = view.ele("Shell") + .ele("NavContainer") + .a({ n: "id", v: "nav" }); + + const main = nav.ele("Page") + .a({ n: "id", v: "page-main" }) + .a({ n: "title", v: "abap2UI5 - Hash - App-Owned Routing (#/detail)" }) + .a({ n: "showNavButton", b: this.client.check_app_prev_stack() }) + .a({ n: "navButtonPress", v: this.client._event_nav_app_leave() }); + + main.tag("MessageStrip") + .a({ n: "text", v: "The URL has no hash right now. Type something, open the detail page and watch " + + "the address bar: hash_set writes #/detail, and the BROWSER Back button returns here " + + "- with the input still set." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + const form = main.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Some state" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }); + + form.tag("Label") + .a({ n: "text", v: "quantity" }); + form.tag("Input") + .a({ n: "value", v: this.client._bind("quantity") }); + + form.tag("Button") + .a({ n: "press", v: this.client._event("GO_DETAIL") }) + .a({ n: "text", v: "open the detail page (#/detail)" }) + .a({ n: "type", v: "Emphasized" }); + + const detail = nav.ele("Page") + .a({ n: "id", v: "page-detail" }) + .a({ n: "title", v: "Detail (#/detail)" }) + .a({ n: "showNavButton", b: true }) + // the router app's nav-back, the UI5 onNavBack pattern: one real, + // CONSUMED step back in the browser history - and on a cold deep + // link, where no step exists, a REPLACE to the '/' fallback. Either + // way the hash change round-trips as HASH_CHANGED below + .a({ n: "navButtonPress", v: this.client.follow_up_action({ val: z2ui5_if_client.cs_event.hash_back, + t_arg: [ "/" ] }) }); + + detail.tag("MessageStrip") + .a({ n: "text", v: "This page is #/detail. Reload the browser or share the URL - it lands here. " + + "The back arrow is cs_event-hash_back with '/' as fallback: normally a real " + + "window.history.go(-1), and on a cold deep link a replace to the start page. " + + "Edit the variant in the URL to something unknown (#/detail/z) and watch it repaired." }) + .a({ n: "type", v: "Success" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + detail.tag("Button") + .a({ n: "press", v: this.client._event("GO_MAIN") }) + .a({ n: "text", v: "start page, pushed as a new entry (#/)" }) + .a({ n: "class", v: "sapUiSmallMarginBegin" }); + + const vform = detail.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "hash_replace - the URL follows, Back skips it" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }); + + vform.tag("Label") + .a({ n: "text", v: "variant (watch the URL - and note Back still returns to the first page)" }); + vform.ele("SegmentedButton") + .a({ n: "selectedKey", v: this.client._bind("variant") }) + .a({ n: "selectionChange", v: this.client._event({ val: "VARIANT", arg: "${$parameters>/item}.getKey()" }) }) + .ele("items") + .tag("SegmentedButtonItem") + .a({ n: "key", v: "a" }) + .a({ n: "text", v: "Variant A" }) + .tag("SegmentedButtonItem") + .a({ n: "key", v: "b" }) + .a({ n: "text", v: "Variant B" }) + .tag("SegmentedButtonItem") + .a({ n: "key", v: "c" }) + .a({ n: "text", v: "Variant C" }) + .end() + .end(); + + detail.tag("ObjectStatus") + .a({ n: "text", v: this.client._bind("quantity") }) + .a({ n: "title", v: "quantity from the first page" }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + this.client.view_display(view.stringify()); + + // hash changes the app did not write itself (browser Back/Forward, a + // manual edit) round-trip as HASH_CHANGED - registered per render, since + // the registration dies with an app switch + this.client.follow_up_action({ val: z2ui5_if_client.cs_event.hash_attach_changed, + t_arg: [ "HASH_CHANGED" ] }); + + // a rebuilt NavContainer is back on its first page while check_detail + // survives as class state - re-issue the page it should show + if (this.check_detail === true) { + this.client.follow_up_action({ val: this.client.cs_event.control_by_id, + t_arg: [ "nav", "to", "page-detail" ] }); + } + + } + + on_event() { + + switch (this.client.get_event()) { + + case "GO_DETAIL": + // the router's navTo: switch the page and PUSH the app-owned hash - + // a real history entry, so the browser Back button has a step to take + this.check_detail = true; + this.client.follow_up_action({ val: this.client.cs_event.control_by_id, + t_arg: [ "nav", "to", "page-detail" ] }); + this.client.hash_set(`/detail/${this.variant}`); + break; + + case "GO_MAIN": + // the EVENT form of hash_set( ): follow_up_action with + // cs_event-hash_set writes the same field the typed method writes - + // a pushed history entry, so Back returns to the detail page + this.check_detail = false; + this.client.follow_up_action({ val: this.client.cs_event.control_by_id, + t_arg: [ "nav", "to", "page-main" ] }); + this.client.follow_up_action({ val: this.client.cs_event.hash_set, + t_arg: [ "/" ] }); + break; + + case "VARIANT": + // the router's replace-navTo: the URL follows the variant WITHOUT a + // new history entry - Back keeps returning to the first page, not + // through every variant ever clicked + this.variant = this.client.get_event_arg(); + this.client.hash_replace(`/detail/${this.variant}`); + break; + + case "HASH_CHANGED": + // the router's routeMatched: show the page the hash now names + this.hash_apply(); + + // a hash that names a variant the app does not have (#/detail/z, + // typed by hand) is repaired IN PLACE - the event form of + // hash_replace( ), no history entry for the broken URL + if (this.check_detail === true && this.client.get().s_config.hash !== `/detail/${this.variant}`) { + this.client.follow_up_action({ val: this.client.cs_event.hash_replace, + t_arg: [ `/detail/${this.variant}` ] }); + } + + this.client.follow_up_action({ val: this.client.cs_event.control_by_id, + t_arg: [ "nav", + "to", + (this.check_detail === true ? "page-detail" + : "page-main") ] }); + break; + + } + + } + + hash_apply() { + + // '/detail' or '/detail/{variant}' - everything else is the first page + const lv_hash = this.client.get().s_config.hash; + this.check_detail = (lv_hash.toUpperCase().includes("/detail".toUpperCase())); + if (this.check_detail === true) { + const lv_variant = (lv_hash.includes(("/detail/")) ? lv_hash.slice(lv_hash.indexOf(("/detail/")) + ("/detail/").length) : ""); + if ([...lv_variant].every((c) => "abc".includes(c)) && lv_variant !== "") { + this.variant = lv_variant; + } + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_503.js b/srv/apps/z2ui5_cl_smp_app_503.js new file mode 100644 index 0000000..c72a9e7 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_503.js @@ -0,0 +1,103 @@ +// @keywords unit test abapunit testclasses assert testable logic method +// @summary The app logic in a method of its own - no client, no attributes - so the local test class beside the class can assert it with ABAP Unit. +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_503.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_503", class { + + amount = 0; + rate = 0; + gross = ""; + + main(client) { + + this.client = client; + + if (client.check_on_init()) { + + this.amount = 100; + this.rate = 19; + + this.view_display(); + + } else if (client.check_on_navigated()) { + this.view_display(); + + } else if (client.check_on_event("CALC")) { + this.gross = `${this.gross_amount({ net: this.amount, percent: this.rate })}`; + } + + } + + // The logic under test: a net amount plus a percentage, rounded to whole + // units. It takes what it needs and returns what it computes - it reads + // no attribute, touches no client and starts no roundtrip, which is the + // whole reason a test can call it. + // + // @parameter net | the net amount + // @parameter percent | the tax rate, in percent + // @parameter result | the gross amount, rounded to whole units + gross_amount({ net, percent } = {}) { + let result = 0; + + // the rounding is the part worth a test: + 50 before the integer division + // rounds the half up instead of cutting it off + result = net + Math.floor((net * percent + 50) / 100); + + return result; + } + + 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:form", v: "sap.ui.layout.form" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Basics VI - Unit Tests for the App Logic" }) + .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: "The sum below is computed by gross_amount( ), a method that takes what it needs and " + + "returns what it computes - it reads no attribute and never sees the client. That is what makes " + + "it testable: the local test class in z2ui5_cl_smp_app_503.clas.testclasses.abap calls it with " + + "three inputs and asserts the three answers, without a browser, a view or a roundtrip. abapGit " + + "keeps that file beside the class, and CLSCCINCL in the .clas.xml is what says the class has one." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Gross from net" }) + .a({ n: "editable", b: true }) + .a({ n: "layout", v: "ResponsiveGridLayout" }) + .ele({ n: "content", ns: "form" }) + .tag("Label") + .a({ n: "text", v: "Net amount" }) + .tag("Input") + .a({ n: "value", v: this.client._bind("amount") }) + .tag("Label") + .a({ n: "text", v: "Tax rate in percent" }) + .tag("Input") + .a({ n: "value", v: this.client._bind("rate") }) + .tag("Label") + .a({ n: "text", v: "Gross amount" }) + .tag("Text") + .a({ n: "text", v: this.client._bind("gross") }) + .tag("Label") + .a({ n: "text", v: "Run the logic" }) + .tag("Button") + .a({ n: "text", v: "gross_amount( )" }) + .a({ n: "type", v: "Emphasized" }) + .a({ n: "press", v: this.client._event("CALC") }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_506.js b/srv/apps/z2ui5_cl_smp_app_506.js new file mode 100644 index 0000000..9bf194e --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_506.js @@ -0,0 +1,143 @@ +// @keywords event argument literal quoted expression evaluated check_arg_literal t_arg data binding syntax dollar brace +// @summary An event argument that starts with $ or { is live UI5 expression syntax and gets resolved on the client - check_arg_literal sends it as the text it is. +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_506.clas.abap +// +// Every argument of an _event( ) wire is written into the view. One that +// starts with `$` or `{` is written RAW, as live UI5 expression syntax - +// that is how `${$source>/KEY}` reaches the handler as the row's value +// instead of as those thirteen characters. +// +// Data can start with those characters too: text a user typed, a key from +// a foreign system. Written raw it is RESOLVED, and what arrives is the +// result - the bound price instead of the text `${/PRICE}`. s_ctrl- +// check_arg_literal quotes every argument of that wire as a string, so the +// wire gives up expressions and carries data. Two buttons over the same +// argument show both readings side by side. +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_506", class { + + price = ""; + argument = ""; + raw_arg = ""; + lit_arg = ""; + + main(client) { + + this.client = client; + if (client.check_on_init()) { + + this.price = "1299.00"; + // the argument both wires start with: the client-side spelling of the + // bound price, composed from the bind so the path is a registered one + this.argument = `\${${client._bind({ val: "price", path: true })}}`; + this.raw_arg = "-"; + this.lit_arg = "-"; + this.view_display(); + + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + on_event() { + + switch (this.client.get_event()) { + + case "RAW": + // what the client resolved: `${/PRICE}` names the bound price, so + // its value arrives, not the text + this.raw_arg = this.client.get_event_arg(); + break; + + case "LITERAL": + // the same argument, quoted by check_arg_literal: the text arrives + this.lit_arg = this.client.get_event_arg(); + break; + + case "REBUILD": + // the argument is written into the view at render time, so a new + // text needs a new view - the two wires below carry it from then on + this.raw_arg = "-"; + this.lit_arg = "-"; + this.view_display(); + break; + + } + + } + + 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:form", v: "sap.ui.layout.form" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Event - Literal Arguments (check_arg_literal)" }) + .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", t: "Both buttons send the same argument. Written raw, the $-expression is resolved and " + + "the bound price arrives; with s_ctrl-check_arg_literal the wire quotes it and the text " + + "arrives. Type any other text - $event, {= 1 + 1 } - press Rebuild, and the literal " + + "wire still delivers it unchanged." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + const form = page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "One argument, two wires" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }); + + form.tag("Label") + .a({ n: "text", v: "The bound price the raw argument resolves to" }); + form.tag("Input") + .a({ n: "value", v: this.client._bind("price") }); + + form.tag("Label") + .a({ n: "text", v: "The argument both buttons carry" }); + form.tag("Input") + .a({ n: "value", v: this.client._bind("argument") }); + form.tag("Button") + .a({ n: "text", v: "Rebuild the view with this argument" }) + .a({ n: "press", v: this.client._event("REBUILD") }); + + // the raw wire is kept on the binding shape the sample starts with: any + // other text typed above is only sent through the literal wire, because + // an argument that is not valid expression syntax would break the view + const raw_expr = `\${${this.client._bind({ val: "price", path: true })}}`; + form.tag("Label") + .a({ n: "text", v: "Raw - resolved on the client" }); + form.tag("Button") + .a({ n: "text", t: `Send ${raw_expr} raw` }) + .a({ n: "press", v: this.client._event({ val: "RAW", + arg: raw_expr }) }); + form.tag("Text") + .a({ n: "text", v: this.client._bind("raw_arg") }); + + form.tag("Label") + .a({ n: "text", v: "Literal - quoted by check_arg_literal" }); + form.tag("Button") + .a({ n: "text", t: `Send ${this.argument} as a literal` }) + .a({ n: "type", v: "Emphasized" }) + .a({ n: "press", v: this.client._event({ val: "LITERAL", + arg: this.argument, + s_ctrl: { check_arg_literal: true } }) }); + form.tag("Text") + .a({ n: "text", v: this.client._bind("lit_arg") }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_507.js b/srv/apps/z2ui5_cl_smp_app_507.js new file mode 100644 index 0000000..c1bbbe4 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_507.js @@ -0,0 +1,130 @@ +// @keywords binding omit initial default property absent omit_initial omit_initial_paths ratingindicator maxvalue enabled boolean +// @summary An ABAP field is never absent, only initial - so a bound 0 overrides the UI5 default. omit_initial leaves initial fields out of the model, omit_initial_paths only the ones you name. +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_507.clas.abap +// +// Three lists over the same rows, bound three ways. Each RatingIndicator +// reads maxValue and enabled from its row: +// +// - plain _bind( ): every field reaches the client as a value, so a row +// with no maxValue sends 0 and the control shows no stars - the UI5 +// default of 5 never had a chance +// - omit_initial = abap_true: initial fields are left out, the default +// applies - but abap_false is initial too, so the archived row loses +// its enabled = false and becomes editable +// - omit_initial_paths = MAXVALUE: only that column is omitted, the +// boolean still travels, and both rows render as intended +import { defineApp, t, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +const ty_s_row = { + name: "", + value: 0, + maxvalue: 0, + enabled: false, + note: "", +}; + +defineApp("Z2UI5_CL_SMP_APP_507", class { + + t_plain = t.table(ty_s_row); + t_omit = t.table(ty_s_row); + t_paths = t.table(ty_s_row); + + main(client) { + + this.client = client; + if (client.check_on_init()) { + + // the same rows three times - one attribute per binding, because the + // omission is a property of the bind, not of the data + this.t_plain = [ + { name: "Handling", value: 4, maxvalue: 5, enabled: true, note: "maxValue 5, enabled" }, + { name: "Price", value: 7, maxvalue: 10, enabled: true, note: "maxValue 10, enabled" }, + { name: "Design", value: 3, enabled: true, note: "NO maxValue - the UI5 default 5 is meant" }, + { name: "Archived", value: 2, maxvalue: 5, enabled: false, note: "maxValue 5, NOT enabled" } ]; + this.t_omit = this.t_plain; + this.t_paths = this.t_plain; + this.view_display(); + + } else if (client.check_on_navigated()) { + this.view_display(); + } + + } + + 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" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Binding - Omit Initial Values" }) + .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: "The same four rows, bound three ways. Design has no maxValue and Archived is not " + + "enabled - watch what each binding makes of the two: a plain bind sends 0 for the missing " + + "maxValue, omit_initial drops it but drops the abap_false as well, omit_initial_paths " + + "drops only the column you name." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + const row = page.ele("HBox") + .a({ n: "wrap", v: "Wrap" }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + this.render_list({ parent: row, + title: "_bind( t_plain )", + descr: "every field travels: Design gets maxValue 0 and shows no stars", + items: this.client._bind("t_plain") }); + + this.render_list({ parent: row, + title: "_bind( t_omit omit_initial = abap_true )", + descr: "initial fields stay out: Design shows 5 stars - but Archived is enabled now, its abap_false was initial too", + items: this.client._bind({ val: "t_omit", + omit_initial: true }) }); + + this.render_list({ parent: row, + title: "_bind( t_paths omit_initial_paths = MAXVALUE )", + descr: "only MAXVALUE stays out: Design shows 5 stars and Archived stays disabled", + items: this.client._bind({ val: "t_paths", + omit_initial_paths: [ "MAXVALUE" ] }) }); + + this.client.view_display(view.stringify()); + + } + + render_list({ parent, title, descr, items } = {}) { + + const box = parent.ele("VBox") + .a({ n: "width", v: "22rem" }) + .a({ n: "class", v: "sapUiSmallMarginEnd sapUiSmallMarginBottom" }); + + box.tag("Title") + .a({ n: "text", t: title }) + .a({ n: "level", v: "H4" }); + box.tag("Text") + .a({ n: "text", t: descr }) + .a({ n: "class", v: "sapUiTinyMarginBottom" }); + + box.ele("List") + .a({ n: "items", v: items }) + .ele("CustomListItem") + .ele("VBox") + .a({ n: "class", v: "sapUiTinyMargin" }) + .tag("Label") + .a({ n: "text", v: "{NAME} - {NOTE}" }) + .tag("RatingIndicator") + .a({ n: "value", v: "{VALUE}" }) + .a({ n: "maxValue", v: "{MAXVALUE}" }) + .a({ n: "enabled", v: "{ENABLED}" }) + .a({ n: "iconSize", v: "1.25rem" }); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_508.js b/srv/apps/z2ui5_cl_smp_app_508.js new file mode 100644 index 0000000..6a202e9 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_508.js @@ -0,0 +1,106 @@ +// @keywords binding path only bare path _bind_path expression binding sorter binding info composed raw string +// @summary _bind_path( ) hands you the model PATH of an attribute instead of {/PATH}: the piece a composed expression binding, a sorter or a bindElement needs. +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_508.clas.abap +// +// `client->_bind( quantity )` returns `{/QUANTITY}` - a complete property +// binding, ready for a view attribute. Some attributes need the PATH alone: +// an expression that combines it with a comparison, a binding-info object +// that adds a sorter or a formatter, a bindElement. `client->_bind_path( +// quantity )` returns `/QUANTITY` for exactly that - byte for byte what +// `_bind( val = quantity path = abap_true )` returns, under a name that +// says what it does. It registers the attribute like every bind: a path +// the view names must be in the model, or the client renders nothing. +import { defineApp, t, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +const ty_s_row = { + product: "", + stock: 0, +}; + +defineApp("Z2UI5_CL_SMP_APP_508", class { + + t_row = t.table(ty_s_row); + + quantity = 0; + + main(client) { + + this.client = client; + if (client.check_on_init()) { + + this.quantity = 120; + this.t_row = [ { product: "Monitor 27\"", stock: 5 }, + { product: "Headset", stock: 9 }, + { product: "Keyboard", stock: 71 }, + { product: "Notebook 15\"", stock: 12 } ]; + this.view_display(); + + } else if (client.check_on_navigated()) { + this.view_display(); + } + + } + + 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:form", v: "sap.ui.layout.form" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Binding - Path Only (_bind_path)" }) + .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: "_bind( ) returns the finished binding string; _bind_path( ) returns the bare path for the " + + "places that compose their own binding string: the expression that colours the status " + + "below, and the items binding of the list, which adds a sorter to the path. Change the " + + "quantity and press Enter to see the expression re-evaluate on the client." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + const form = page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "One attribute, two spellings" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }); + + form.tag("Label") + .a({ n: "text", v: "quantity, bound with _bind( ) - the full binding" }); + form.tag("Input") + .a({ n: "value", v: this.client._bind("quantity") }) + .a({ n: "description", t: this.client._bind("quantity") }); + + // the bare path inside an expression binding: the string is composed in + // ABAP, and the path in it comes from _bind_path( ) rather than being + // written by hand - a hand-written path is not in the model + form.tag("Label") + .a({ n: "text", v: "the same attribute inside an expression - needs the bare path" }); + form.tag("ObjectStatus") + .a({ n: "text", v: `{= \${${this.client._bind_path("quantity")}} > 100 ? 'more than 100 in stock' : 'running low' }` }) + .a({ n: "state", v: `{= \${${this.client._bind_path("quantity")}} > 100 ? 'Success' : 'Warning' }` }); + form.tag("Text") + .a({ n: "text", t: `_bind_path( quantity ) returned ${this.client._bind_path("quantity")}` }); + + // the bare path of a table inside a binding-info object: the sorter is + // client-side, the path still names a bound attribute + form.tag("Label") + .a({ n: "text", v: "a table path with a sorter added - needs the bare path" }); + form.ele("List") + .a({ n: "items", v: `{ path: '${this.client._bind_path("t_row")}', sorter: { path: 'PRODUCT' }, templateShareable: false }` }) + .tag("StandardListItem") + .a({ n: "title", v: "{PRODUCT}" }) + .a({ n: "info", v: "{STOCK} in stock" }); + form.tag("Text") + .a({ n: "text", t: `_bind_path( t_row ) returned ${this.client._bind_path("t_row")} - the list is sorted by product on the client` }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_509.js b/srv/apps/z2ui5_cl_smp_app_509.js new file mode 100644 index 0000000..e3029d5 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_509.js @@ -0,0 +1,105 @@ +// @keywords binding json string spliced model node object array pre-serialized _bind json = abap_true no abap type +// @summary A string that already holds JSON is spliced into the model as a node instead of a quoted string - a list bound to an array nobody declared an ABAP type for. +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_509.clas.abap +// +// Every bound value is serialized from its ABAP type. A string that +// already CONTAINS JSON - read from a table column, returned by a service, +// written by hand - would arrive as one quoted string. `_bind( val = x +// json = abap_true )` splices it into the model as a JSON node instead, so +// a list binds to the array and a text to a member, and no ABAP structure +// has to mirror keys that may not even be valid ABAP names. Outbound only: +// the client sends nothing back into that node. +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_509", class { + + products_json = ""; + config_json = ""; + products_raw = ""; + + main(client) { + + this.client = client; + if (client.check_on_init()) { + + // what a service or a JSON column would hand over: an array with keys + // no ABAP type declares, and an object with a key (`sap.app`) that no + // ABAP component could even be named after + this.products_json = "[ { \"name\": \"Notebook 15\\\"\", \"price\": 1299, \"tags\": \"hardware, mobile\" }," + + " { \"name\": \"USB-C Dock\", \"price\": 189, \"tags\": \"accessories\" }," + + " { \"name\": \"Headset\", \"price\": 79, \"tags\": \"audio, accessories\" } ]"; + this.config_json = "{ \"title\": \"Products from JSON\", \"sap.app\": { \"id\": \"z2ui5.demo\", \"version\": \"1.0.0\" } }"; + this.products_raw = this.products_json; + this.view_display(); + + } else if (client.check_on_navigated()) { + this.view_display(); + } + + } + + 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:form", v: "sap.ui.layout.form" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Binding - Pre-serialized JSON (json)" }) + .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: "Two strings hold JSON the app never parsed. Bound with json = abap_true they become " + + "model nodes: the list binds to the array, the title reads a member of the object - even " + + "one called sap.app. The same string bound the ordinary way arrives as text, which is " + + "what the last field shows." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + const form = page.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "title", v: "Spliced in as JSON" }) + .a({ n: "editable", b: true }) + .ele({ n: "content", ns: "form" }); + + form.tag("Label") + .a({ n: "text", v: "a member of the object - its title" }); + // the bare path of the spliced object, so the view can reach into it + form.tag("Text") + // the member lives inside the JSON string, which only the client parses + .a({ n: "text", v: `{${this.client._bind({ val: "config_json", json: true, path: true })}/title}` }); + + form.tag("Label") + .a({ n: "text", v: "a key no ABAP component could carry - sap.app/id" }); + form.tag("Text") + .a({ n: "text", v: `{${this.client._bind({ val: "config_json", json: true, path: true })}/sap.app/id} version ` + + `{${this.client._bind({ val: "config_json", json: true, path: true })}/sap.app/version}` }); + + form.tag("Label") + .a({ n: "text", v: "the array, as an aggregation binding" }); + form.ele("List") + .a({ n: "items", v: this.client._bind({ val: "products_json", + json: true }) }) + .tag("StandardListItem") + .a({ n: "title", v: "{name}" }) + .a({ n: "description", v: "{tags}" }) + .a({ n: "info", v: "{price} EUR" }); + + form.tag("Label") + .a({ n: "text", v: "the same string, bound without json - one quoted string" }); + form.tag("TextArea") + .a({ n: "value", v: this.client._bind("products_raw") }) + .a({ n: "editable", b: false }) + .a({ n: "rows", v: "4" }) + .a({ n: "width", v: "100%" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_510.js b/srv/apps/z2ui5_cl_smp_app_510.js new file mode 100644 index 0000000..a21e194 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_510.js @@ -0,0 +1,193 @@ +// @keywords nested view destroy nest_view_destroy nest2_view_destroy slot scope cs_view nested nested2 control_by_id focus +// @summary Two nested views built and torn down on demand, and a frontend action aimed at one of them by its slot - the view parameter of follow_up_action. +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_510.clas.abap +// +// Two containers on the main page, one nested view each. nest_view_display +// puts a view INTO a container, nest_view_destroy takes it out again and +// leaves the container - the same for the second slot with nest2_*. +// +// A control_by_id action resolves an id across every open view by default +// (cs_view-main). The view parameter of follow_up_action scopes the lookup +// to ONE slot - cs_view-nested or cs_view-nested2 - so the focus is looked +// for in the view the app meant and nowhere else: a slot that is not open +// answers with nothing instead of the first match somewhere else. +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_510", class { + + input_nest = ""; + input_nest2 = ""; + status = ""; + + main(client) { + + this.client = client; + if (client.check_on_init()) { + + this.status = "no nested view yet"; + this.view_display(); + + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + on_event() { + + switch (this.client.get_event()) { + + case "NEST_SHOW": + this.nest_display(z2ui5_if_client.cs_view.nested); + this.status = "nested view 1 rendered into box_nest"; + break; + + case "NEST_DESTROY": + // the view goes, the container stays - and the bound input keeps + // its value in the class, so a new display brings it back + this.client.nest_view_destroy(); + this.status = "nested view 1 destroyed - box_nest is empty again"; + break; + + case "NEST2_SHOW": + this.nest_display(z2ui5_if_client.cs_view.nested2); + this.status = "nested view 2 rendered into box_nest2"; + break; + + case "NEST2_DESTROY": + this.client.nest2_view_destroy(); + this.status = "nested view 2 destroyed - box_nest2 is empty again"; + break; + + case "FOCUS_NEST": + // the slot decides where the lookup happens: cs_view-nested is the + // first nested view and nothing else. Without the view parameter + // the id would be searched across every open view + this.client.follow_up_action({ val: z2ui5_if_client.cs_event.control_by_id, + view: z2ui5_if_client.cs_view.nested, + t_arg: [ "inp_nest", "focus" ] }); + this.status = "focus sent to id inp_nest, scoped to cs_view-nested"; + break; + + case "FOCUS_NEST2": + this.client.follow_up_action({ val: z2ui5_if_client.cs_event.control_by_id, + view: z2ui5_if_client.cs_view.nested2, + t_arg: [ "inp_nest2", "focus" ] }); + this.status = "focus sent to id inp_nest2, scoped to cs_view-nested2"; + break; + + } + + } + + nest_display(slot) { + + const view = z2ui5_cl_ui5_view_builder.factory() + .ele({ n: "View", ns: "mvc" }) + .a({ n: "displayBlock", v: "true" }) + .a({ n: "xmlns", v: "sap.m" }) + .a({ n: "xmlns:mvc", v: "sap.ui.core.mvc" }); + + const panel = view.ele("Panel") + .a({ n: "headerText", t: `nested view in slot ${slot}` }) + .a({ n: "class", v: "sapUiSmallMarginTop" }); + + if (slot === z2ui5_if_client.cs_view.nested) { + + panel.tag("Input") + .a({ n: "id", v: "inp_nest" }) + .a({ n: "placeholder", v: "id inp_nest, slot NEST" }) + .a({ n: "value", v: this.client._bind("input_nest") }); + + this.client.nest_view_display({ val: view.stringify(), + id: "box_nest", + method_insert: "addItem", + method_destroy: "removeAllItems" }); + + } else { + + panel.tag("Input") + .a({ n: "id", v: "inp_nest2" }) + .a({ n: "placeholder", v: "id inp_nest2, slot NEST2" }) + .a({ n: "value", v: this.client._bind("input_nest2") }); + + this.client.nest2_view_display({ val: view.stringify(), + id: "box_nest2", + method_insert: "addItem", + method_destroy: "removeAllItems" }); + + } + + } + + 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" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Nested View - Destroy and Target a Slot" }) + .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: "Show and destroy the two nested views, then send the focus into one of them: the " + + "view parameter of follow_up_action decides which slot the id lookup is scoped to - " + + "a slot that is not open finds nothing." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele("HBox") + .a({ n: "wrap", v: "Wrap" }) + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("Button") + .a({ n: "text", v: "show nested view 1" }) + .a({ n: "press", v: this.client._event("NEST_SHOW") }) + .tag("Button") + .a({ n: "text", v: "destroy nested view 1" }) + .a({ n: "press", v: this.client._event("NEST_DESTROY") }) + .tag("Button") + .a({ n: "text", v: "focus the input in slot nested" }) + .a({ n: "press", v: this.client._event("FOCUS_NEST") }) + .tag("ToolbarSpacer") + .a({ n: "width", v: "1rem" }) + .tag("Button") + .a({ n: "text", v: "show nested view 2" }) + .a({ n: "press", v: this.client._event("NEST2_SHOW") }) + .tag("Button") + .a({ n: "text", v: "destroy nested view 2" }) + .a({ n: "press", v: this.client._event("NEST2_DESTROY") }) + .tag("Button") + .a({ n: "text", v: "focus the input in slot nested2" }) + .a({ n: "press", v: this.client._event("FOCUS_NEST2") }); + + page.tag("MessageStrip") + .a({ n: "text", v: this.client._bind("status") }) + .a({ n: "type", v: "Success" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + // the two containers the nested views are inserted into - a VBox each, + // addItem inserts, removeAllItems clears before a repeated display + page.ele("HBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("VBox") + .a({ n: "id", v: "box_nest" }) + .a({ n: "width", v: "20rem" }) + .a({ n: "class", v: "sapUiSmallMarginEnd" }) + .tag("VBox") + .a({ n: "id", v: "box_nest2" }) + .a({ n: "width", v: "20rem" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_511.js b/srv/apps/z2ui5_cl_smp_app_511.js new file mode 100644 index 0000000..ea1afdc --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_511.js @@ -0,0 +1,131 @@ +// @keywords livechange keystroke queue busy roundtrip dropped event check_queue_last s_ctrl +// @summary Two identical liveChange wires side by side: the plain one drops every keystroke typed while a round-trip runs, the one registered with check_queue_last keeps the last of them, so the backend ends on what you typed. +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_511.clas.abap +// +// abap2UI5 serializes round-trips: while one is in flight the app is busy and +// an event fired meanwhile is DROPPED. Right for a click, wrong for a wire +// that fires per keystroke - typing `abc` at once leaves the backend at `a` +// until the user pauses and types again. s_ctrl-check_queue_last keeps the +// LAST event fired during the flight in a one-slot buffer and dispatches it +// once the response has landed: one round-trip at a time, order preserved, +// the backend ends on the control's current value. +// +// Needs abap2UI5 newer than 1.144.0 - check_queue_last is appended to +// ty_s_event_control after that release; on an older framework the class +// does not activate (unknown component of s_ctrl). +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_511", class { + + plain_backend = ""; + plain_count = 0; + queued_backend = ""; + queued_count = 0; + + main(client) { + + this.client = client; + if (client.check_on_navigated()) { + this.view_display(); + + } else if (client.check_on_event("PLAIN")) { + + this.plain_backend = client.get_event_arg(); + this.plain_count = this.plain_count + 1; + + } else if (client.check_on_event("QUEUED")) { + + this.queued_backend = client.get_event_arg(); + this.queued_count = this.queued_count + 1; + + } + + } + + view_display() { + + // Both Inputs below round-trip on every keystroke on purpose: this sample + // EXISTS to show what the busy guard does to such a wire, and what the + // check_queue_last flag changes about it. + + 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:layout", v: "sap.ui.layout" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Event - Keep the Last Keystroke with check_queue_last" }) + .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: "abap2UI5 runs one round-trip at a time, and an event fired while one is in flight is dropped. " + + "Type quickly into both fields: the left backend value stops at an earlier keystroke, the right one " + + "ends on what you typed - its wire is registered with s_ctrl-check_queue_last, so the last event " + + "fired during the flight is kept and sent once the response has landed. The flag is for " + + "per-keystroke wires only (liveChange, liveSearch, sliderChange); Z2UI5_CL_SMP_APP_059 puts it to work on " + + "a live search over a large table." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + // The Inputs are deliberately NOT bound: the keystroke travels as the + // event argument ${$parameters>/value} and nowhere else, so what the + // backend Text shows is exactly what the wire delivered. + const grid = page.ele({ n: "Grid", ns: "layout" }) + .a({ n: "defaultSpan", v: "XL6 L6 M6 S12" }) + .ele({ n: "content", ns: "layout" }); + + // left: the plain wire - keystrokes typed while a round-trip runs are dropped + grid.ele("VBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("Title") + .a({ n: "text", v: "Plain wire - dropped while busy" }) + .a({ n: "level", v: "H4" }) + .tag("Input") + .a({ n: "placeholder", v: "Type quickly ..." }) + .a({ n: "liveChange", v: this.client._event({ + val: "PLAIN", + arg: "${$parameters>/value}" }) }) + .tag("Label") + .a({ n: "text", v: "Value in the backend" }) + .tag("Text") + .a({ n: "text", v: this.client._bind("plain_backend") }) + .tag("Label") + .a({ n: "text", v: "Round-trips" }) + .tag("Text") + .a({ n: "text", v: this.client._bind("plain_count") }); + + // right: the same wire with check_queue_last - the last keystroke of the + // flight is kept and dispatched after the response; check_no_busy keeps + // the busy overlay from covering the field while it is typed into + grid.ele("VBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("Title") + .a({ n: "text", v: "check_queue_last - the last keystroke is kept" }) + .a({ n: "level", v: "H4" }) + .tag("Input") + .a({ n: "placeholder", v: "Type quickly ..." }) + .a({ n: "liveChange", v: this.client._event({ + val: "QUEUED", + arg: "${$parameters>/value}", + s_ctrl: { check_queue_last: true, + check_no_busy: true } }) }) + .tag("Label") + .a({ n: "text", v: "Value in the backend" }) + .tag("Text") + .a({ n: "text", v: this.client._bind("queued_backend") }) + .tag("Label") + .a({ n: "text", v: "Round-trips" }) + .tag("Text") + .a({ n: "text", v: this.client._bind("queued_count") }); + + this.client.view_display(view.stringify()); + + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_513.js b/srv/apps/z2ui5_cl_smp_app_513.js new file mode 100644 index 0000000..078ef80 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_513.js @@ -0,0 +1,130 @@ +// @keywords css inline style background color opacity control_by_id dom node no property +// @summary Writes a whitelisted CSS declaration onto a control's own DOM node with the css control method - for a value the control has no property for. +// @docs https://abap2ui5.github.io/docs/cookbook/event_navigation/frontend +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_513.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_513", class { + + // not bound - mirrors what was written last, so the buttons can alternate + highlighted = false; + + main(client) { + + this.client = client; + if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + css_set({ property, value } = {}) { + + // t_arg is positional: id, the pseudo-method `css`, the property, the value. + // `css` is not a UI5 method - it is a frontend capability in method form, + // and only ten properties are allowed: width, min-width, max-width, + // height, min-height, max-height, color, background-color, font-size and + // opacity. Anything else is refused and logged, never applied. + this.client.follow_up_action({ val: z2ui5_if_client.cs_event.control_by_id, + t_arg: [ "demoPanel", + "css", + property, + value ] }); + + } + + on_event() { + + switch (this.client.get_event()) { + + case "HIGHLIGHT": + this.highlighted = (this.highlighted === false); + // sap.m.Panel carries no background colour and no font size, so there + // is no property to bind and nothing a formatter could reach. The + // declaration lands on the panel's own DOM node. + if (this.highlighted === true) { + this.css_set({ property: "background-color", + value: "#fff4e5" }); + this.css_set({ property: "font-size", + value: "1.25rem" }); + } else { + // an empty value removes the declaration again + this.css_set({ property: "background-color", + value: "" }); + this.css_set({ property: "font-size", + value: "" }); + } + break; + + case "FADE": + this.css_set({ property: "opacity", + value: "0.35" }); + break; + + case "RESET": + // the declaration lives on the element and is gone after a re-render, + // so a fresh view is the other way back to the control's own styling + this.highlighted = false; + this.view_display(); + break; + + } + + } + + 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" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Control Behaviour - Inline CSS on a Control (css)" }) + .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: "The css control method writes ONE whitelisted declaration onto the control's own DOM node " + + "- for a value the control has no property for. Prefer a bound property wherever one exists; a declaration " + + "written here is gone after the next re-render." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele("VBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .ele("HBox") + .tag("Button") + .a({ n: "press", v: this.client._event("HIGHLIGHT") }) + .a({ n: "text", v: "Toggle background and font size" }) + .a({ n: "icon", v: "sap-icon://palette" }) + .a({ n: "class", v: "sapUiTinyMarginEnd" }) + .tag("Button") + .a({ n: "press", v: this.client._event("FADE") }) + .a({ n: "text", v: "Fade" }) + .a({ n: "icon", v: "sap-icon://hide" }) + .a({ n: "class", v: "sapUiTinyMarginEnd" }) + .tag("Button") + .a({ n: "press", v: this.client._event("RESET") }) + .a({ n: "text", v: "Rebuild the view" }) + .a({ n: "icon", v: "sap-icon://refresh" }); + + page.ele("Panel") + .a({ n: "width", v: "auto" }) + .a({ n: "id", v: "demoPanel" }) + .a({ n: "class", v: "sapUiSmallMargin" }) + .a({ n: "headerText", v: "A panel with no colour property" }) + .tag("Text") + .a({ n: "text", v: "sap.m.Panel has no background-color, no font-size and no opacity. " + + "Every one of them is set here from ABAP, client-side after the response renders." }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_514.js b/srv/apps/z2ui5_cl_smp_app_514.js new file mode 100644 index 0000000..1296029 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_514.js @@ -0,0 +1,148 @@ +// @keywords carousel aggregation item index clone template setactivepage positional control_by_id +// @summary Jumps a Carousel to a page that was cloned from a bound template - addressed positionally as id/aggregation/index, the only way to reach a clone. +// @docs https://abap2ui5.github.io/docs/cookbook/event_navigation/frontend +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_514.clas.abap +import { defineApp, t, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +const ty_s_page = { + title: "", + text: "", +}; + +defineApp("Z2UI5_CL_SMP_APP_514", class { + + t_pages = t.table(ty_s_page); + + // not bound - the index the backend last jumped to, counted from 1 + current = 0; + + 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()) { + this.on_event(); + } + + } + + model_init() { + + this.t_pages = [ { title: "Bicycle", text: "The first page of the carousel." }, + { title: "Car", text: "The second page - and the one the buttons below jump to." }, + { title: "Train", text: "The third page." }, + { title: "Aircraft", text: "The fourth and last page." } ]; + this.current = 1; + + } + + page_show(index) { + + this.current = index; + // A page of this carousel is a CLONE of the aggregation template, and a + // clone has no id the backend can spell: UI5 mints it from the template + // id, the parent id and the position, and the parent id carries the view + // prefix assigned at runtime. So the item is addressed POSITIONALLY - + // `//`, 0-based - which the frontend + // resolves against the live aggregation. It is the equivalent of the UI5 + // controller idiom oCarousel.setActivePage( oCarousel.getPages()[ i ] ). + this.client.follow_up_action({ val: z2ui5_if_client.cs_event.control_by_id, + t_arg: [ "demoCarousel", + "setActivePage", + `demoCarousel/pages/${index - 1}` ] }); + + } + + on_event() { + + switch (this.client.get_event()) { + + case "FIRST": + this.page_show(1); + break; + + case "NEXT": + // wrap around at the end - the count is ABAP's, not the carousel's + this.page_show((this.current >= this.t_pages.length ? 1 : this.current + 1)); + break; + + case "LAST": + this.page_show(this.t_pages.length); + break; + + } + + } + + 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" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Control Behaviour - Aggregation Item by Index" }) + .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: "The carousel pages are clones of a bound template, so none of them has an id the backend " + + "could name. Wherever a control call takes a control id it also takes an aggregation item, written " + + "id/aggregation/index and counted from 0." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele("HBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("Button") + .a({ n: "press", v: this.client._event("FIRST") }) + .a({ n: "text", v: "First" }) + .a({ n: "icon", v: "sap-icon://close-command-field" }) + .a({ n: "class", v: "sapUiTinyMarginEnd" }) + .tag("Button") + .a({ n: "press", v: this.client._event("NEXT") }) + .a({ n: "text", v: "Next" }) + .a({ n: "icon", v: "sap-icon://navigation-right-arrow" }) + .a({ n: "class", v: "sapUiTinyMarginEnd" }) + .tag("Button") + .a({ n: "press", v: this.client._event("LAST") }) + .a({ n: "text", v: "Last" }) + .a({ n: "icon", v: "sap-icon://open-command-field" }); + + page.ele("Carousel") + .a({ n: "id", v: "demoCarousel" }) + .a({ n: "height", v: "20rem" }) + .a({ n: "pages", v: this.client._bind("t_pages") }) + .a({ n: "class", v: "sapUiSmallMargin" }) + .ele("pages") + .ele("VBox") + .a({ n: "justifyContent", v: "Center" }) + .a({ n: "alignItems", v: "Center" }) + .a({ n: "height", v: "100%" }) + .tag("Title") + .a({ n: "text", v: "{TITLE}" }) + .a({ n: "level", v: "H2" }) + .tag("Text") + .a({ n: "text", v: "{TEXT}" }) + .a({ n: "class", v: "sapUiSmallMarginTop" }); + + this.client.view_display(view.stringify()); + + // The active page is live control state that no binding carries: this + // response destroys the view slot and the frontend rebuilds the control + // tree, so the carousel comes back on its first page while `current` + // survives in ABAP. Re-issuing the call from here - after the view that + // will hold it - is what keeps the two in step across a rebuild. + this.page_show(this.current); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_515.js b/srv/apps/z2ui5_cl_smp_app_515.js new file mode 100644 index 0000000..f6ec3f1 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_515.js @@ -0,0 +1,101 @@ +// @keywords busy indicator global control_global show hide blocking wait spinner long running +// @summary Shows and hides the global BusyIndicator from ABAP through control_global - the singleton has no id, so a global target is the only wire that reaches it. +// @docs https://abap2ui5.github.io/docs/cookbook/event_navigation/frontend +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_515.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_515", class { + + status = ""; + + main(client) { + + this.client = client; + if (client.check_on_init()) { + this.status = "Idle - nothing running."; + this.view_display(); + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + busy_call({ method, arg = "" } = {}) { + + // sap.ui.core.BusyIndicator renders nothing of its own and has no id, so + // control_by_id cannot address it - control_global is the only wire that + // reaches a singleton like this one. t_arg is positional: the object, the + // method, then its arguments. show( ) takes the delay in milliseconds + // before the spinner appears; hide( ) takes none. + this.client.follow_up_action({ val: z2ui5_if_client.cs_event.control_global, + t_arg: (arg === "" ? [ "BUSY_INDICATOR", method ] + : [ "BUSY_INDICATOR", method, arg ]) }); + + } + + on_event() { + + switch (this.client.get_event()) { + + case "START": + // The indicator blocks the whole screen, so nothing the user does can + // take it away again - something in this response has to. A client + // timer is what fills the gap here; a real app would hide it in the + // handler of whatever it was waiting for. + this.status = "Running - the BusyIndicator is up, the timer takes it away."; + this.busy_call({ method: "show", + arg: "0" }); + this.client.follow_up_action({ val: z2ui5_if_client.cs_event.start_timer, + t_arg: [ "FINISHED", "2500" ] }); + break; + + case "FINISHED": + this.status = "Done - hide( ) was called from the timer's handler."; + this.busy_call({ method: "hide" }); + break; + + } + + } + + 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" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Control Behaviour - The Global Busy Indicator" }) + .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: "abap2UI5 busies the screen for its own roundtrips already. This is the indicator an app " + + "drives itself, for a wait the framework knows nothing about - the eight whitelisted global objects are " + + "reached the same way, by name instead of by id." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele("VBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("Button") + .a({ n: "press", v: this.client._event("START") }) + .a({ n: "text", v: "Show it for 2.5 seconds" }) + .a({ n: "icon", v: "sap-icon://busy" }) + .a({ n: "type", v: "Emphasized" }) + .tag("ObjectStatus") + .a({ n: "title", v: "Status" }) + .a({ n: "text", v: this.client._bind("status") }) + .a({ n: "class", v: "sapUiSmallMarginTop" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_516.js b/srv/apps/z2ui5_cl_smp_app_516.js new file mode 100644 index 0000000..05e277f --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_516.js @@ -0,0 +1,113 @@ +// @keywords inputmode soft keyboard numeric keypad barcode scanner mobile inputext bound property +// @summary Sets the HTML inputmode of an Input through the bound inputMode property of z2ui5.cc.InputExt - the keyboard layout is model data, not an action. +// @docs https://abap2ui5.github.io/docs/cookbook/browser_interaction/soft_keyboard +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_516.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_516", class { + + mode = ""; + value = ""; + + main(client) { + + this.client = client; + if (client.check_on_init()) { + this.mode = "numeric"; + this.view_display(); + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + on_event() { + + // The mode is an ordinary bound attribute, so switching the keyboard is a + // model update and nothing else - no follow-up action travels, and there + // is no ordering to get right between the action and the next render. + switch (this.client.get_event()) { + case "NUMERIC": + this.mode = "numeric"; + break; + case "DECIMAL": + this.mode = "decimal"; + break; + case "TEL": + this.mode = "tel"; + break; + case "NONE": + this.mode = "none"; + break; + case "OFF": + // empty leaves the field exactly as sap.m.Input rendered it + this.mode = ""; + break; + } + + } + + 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:z2ui5", v: "z2ui5.cc" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Browser - Keyboard Layout of an Input (inputmode)" }) + .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: "inputmode asks the on-screen keyboard for a layout without changing what the field IS - " + + "numeric gives a digit pad on a field that still takes any text, and none keeps the keyboard DOWN while " + + "the field goes on taking input, which is what a barcode scanner needs. Visible on a phone or tablet." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele("VBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag({ n: "InputExt", ns: "z2ui5" }) + .a({ n: "value", v: this.client._bind("value") }) + .a({ n: "inputMode", v: this.client._bind("mode") }) + .a({ n: "placeholder", v: "tap here on a touch device" }) + .a({ n: "width", v: "20rem" }) + .tag("ObjectStatus") + .a({ n: "title", v: "inputMode" }) + .a({ n: "text", v: this.client._bind("mode") }) + .a({ n: "class", v: "sapUiSmallMarginTop sapUiSmallMarginBottom" }); + + page.ele("HBox") + .a({ n: "class", v: "sapUiSmallMarginBegin" }) + .tag("Button") + .a({ n: "press", v: this.client._event("NUMERIC") }) + .a({ n: "text", v: "numeric" }) + .a({ n: "class", v: "sapUiTinyMarginEnd" }) + .tag("Button") + .a({ n: "press", v: this.client._event("DECIMAL") }) + .a({ n: "text", v: "decimal" }) + .a({ n: "class", v: "sapUiTinyMarginEnd" }) + .tag("Button") + .a({ n: "press", v: this.client._event("TEL") }) + .a({ n: "text", v: "tel" }) + .a({ n: "class", v: "sapUiTinyMarginEnd" }) + .tag("Button") + .a({ n: "press", v: this.client._event("NONE") }) + .a({ n: "text", v: "none - no keyboard" }) + .a({ n: "class", v: "sapUiTinyMarginEnd" }) + .tag("Button") + .a({ n: "press", v: this.client._event("OFF") }) + .a({ n: "text", v: "plain sap.m.Input" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_518.js b/srv/apps/z2ui5_cl_smp_app_518.js new file mode 100644 index 0000000..b0935b4 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_518.js @@ -0,0 +1,83 @@ +// @keywords icon font registerfont iconpool tnt collection glyph missing control_global +// @summary Registers the sap.tnt icon collection with IconPool so a sap-icon://SAP-icons-TNT/... URI resolves - without it the icon renders no glyph and logs nothing. +// @docs https://abap2ui5.github.io/docs/cookbook/event_navigation/frontend +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_518.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_518", class { + + main(client) { + + this.client = client; + if (client.check_on_init()) { + this.font_register(); + this.view_display(); + } else if (client.check_on_navigated()) { + this.view_display(); + } + + } + + font_register() { + + // A normal UI5 app does this in its Component's init. An abap2UI5 app has + // no Component of its own, and IconPool is a module SINGLETON rather than + // a control, so no other wire reaches it. t_arg is positional: the font + // family and the font URI - a module path in every real use, resolved + // through sap.ui.require.toUrl, so the registration survives a different + // mount point. + // + // Issue it from the init branch: the collection is registered once per + // session, so a repeat call costs nothing but says the wrong thing. + this.client.follow_up_action({ val: z2ui5_if_client.cs_event.control_global, + t_arg: [ "ICON_POOL", + "registerFont", + "SAP-icons-TNT", + "sap/tnt/themes/base/fonts/" ] }); + + } + + 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 - Control Behaviour - Register an Icon Font" }) + .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: "Only the default SAP-icons font is registered out of the box. A URI naming another " + + "collection renders NO GLYPH and logs nothing at all - an empty space where an icon should be is the " + + "whole symptom, which is what makes this one worth knowing." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + page.ele("VBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("Title") + .a({ n: "text", v: "From the default collection" }) + .a({ n: "level", v: "H3" }) + .tag({ n: "Icon", ns: "core" }) + .a({ n: "src", v: "sap-icon://sap-ui5" }) + .a({ n: "size", v: "2.5rem" }) + .a({ n: "class", v: "sapUiSmallMarginBottom" }) + .tag("Title") + .a({ n: "text", v: "From SAP-icons-TNT, registered above" }) + .a({ n: "level", v: "H3" }) + .tag({ n: "Icon", ns: "core" }) + .a({ n: "src", v: "sap-icon://SAP-icons-TNT/application-service" }) + .a({ n: "size", v: "2.5rem" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_519.js b/srv/apps/z2ui5_cl_smp_app_519.js new file mode 100644 index 0000000..5543bac --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_519.js @@ -0,0 +1,100 @@ +// @keywords translation i18n text element text symbol textpool language message class multi language +// @summary Puts the screen texts in the class's own text elements instead of an i18n file, so SE63 translates them and the app shows them in the logon language. +// @docs https://abap2ui5.github.io/docs/cookbook/translation_messages/translation_i18n +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_519.clas.abap +import { defineApp, z2ui5_cl_ui5_view_builder } from "cap2ui5"; + +defineApp("Z2UI5_CL_SMP_APP_519", class { + + name = ""; + + main(client) { + + this.client = client; + if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event()) { + this.on_event(); + } + + } + + on_event() { + + if (this.client.get_event() === "GREET") { + // The text element is translated, the value is not - so the two are + // concatenated here rather than written as one literal. A text element + // with a placeholder would be a message class instead. + this.client.message_box_display(`${"Hello"} ${this.name}`); + } + + } + + view_display() { + + // the three text symbols this app is about - see the block below + let name_label = ""; + let placeholder = ""; + let greet = ""; + + 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" }); + + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - Basics VII - Translatable Texts (Text Elements)" }) + .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 UI5 app keeps its translations in i18n files. An abap2UI5 app has no frontend " + + "artefacts to put them in - and does not need any: the view is built in ABAP, so ABAP's own " + + "translation carries it. Text elements here, a message class where a text takes placeholders." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + // Every text below comes out of the class's text pool (Goto > Text + // Elements in SE24/ADT). The literal in the source is the fallback and the + // maintenance text; what renders is the entry for the logon language. + // + // Read into a VARIABLE first, and that is part of the lesson: a text + // symbol is a CHARACTER literal, while the builder's v is TYPE string. + // Handing one straight to v answers `'...'(001) is not type-compatible + // with formal parameter V` on a system - a SYNTAX_ERROR of the class, + // although abaplint, the transpiler and the unit suite are all green on + // it. The assignment below is a plain conversion and is allowed on every + // release. Inside a string template ( see on_event( ) ) the symbol needs + // no variable: an embedded expression is a general expression position. + // + // And handed to t, not v: a translation is text somebody else types, and + // t escapes it, so a `{` or `\` in one language's entry is shown rather + // than read by UI5 as a binding. + name_label = "Your name"; + placeholder = "Type a name here"; + greet = "Greet"; + + page.ele("VBox") + .a({ n: "class", v: "sapUiSmallMargin" }) + .tag("Label") + .a({ n: "text", t: name_label }) + .a({ n: "labelFor", v: "nameInput" }) + .tag("Input") + .a({ n: "id", v: "nameInput" }) + .a({ n: "value", v: this.client._bind("name") }) + .a({ n: "placeholder", t: placeholder }) + .a({ n: "width", v: "20rem" }) + .tag("Button") + .a({ n: "press", v: this.client._event("GREET") }) + .a({ n: "text", t: greet }) + .a({ n: "type", v: "Emphasized" }) + .a({ n: "class", v: "sapUiSmallMarginTop" }); + + this.client.view_display(view.stringify()); + + } +}); diff --git a/test/differential.mjs b/test/differential.mjs new file mode 100644 index 0000000..7d1ff0e --- /dev/null +++ b/test/differential.mjs @@ -0,0 +1,106 @@ +// The differential test: every sample beside its ABAP original, in one server. +// +// npm run differential (scripts/originals.mjs first - see there) +// +// The original is abap2UI5's class, transpiled and renamed ZABAP_SMP_APP_nnn +// (test/.originals/); the sample is srv/apps/z2ui5_cl_smp_app_nnn.js, as +// abap2js translated it or a person ported it. Both are started, and then +// 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. +// "Line for line" is a claim about the source; this is the one about what the +// source DOES, and it is the gate a sample passes before it is listed in +// scripts/samples.json. +// +// Two differences are expected, and neither is the sample's: +// - the translation's model carries fields the original's does not send: a +// cap2UI5 app's every field is part of its model (the plugin's README), so +// only the fields the ORIGINAL sends are compared. +// - KNOWN below: what JavaScript does differently on purpose, per sample. +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { test } from "node:test"; +import { pathToFileURL } from "node:url"; +import { actions, post, ROOT, serve } from "./server.mjs"; + +const s = serve(); +const ORIGINALS = path.join(ROOT, "test", ".originals"); +const { generated, handwritten } = JSON.parse(fs.readFileSync(path.join(ROOT, "scripts", "samples.json"), "utf8")); +const SAMPLES = [...generated, ...Object.keys(handwritten)].sort().map((n) => n.toUpperCase()); + +// CONV string( ) of a number: ABAP keeps a sign position ("0 "), String( ) does not +const KNOWN = { Z2UI5_CL_SMP_APP_122: ["DEVICE_HEIGHT", "DEVICE_WIDTH"] }; + +/** The originals, registered into the running runtime as a transpiled class + * registers itself - after the plugin has booted it, which the first + * roundtrip does. */ +async function loadOriginals() { + assert.ok(fs.existsSync(ORIGINALS), "no test/.originals - run node scripts/originals.mjs first"); + await post(s.url, { app: SAMPLES[0] }); + for (const f of fs.readdirSync(ORIGINALS).filter((x) => x.endsWith(".clas.mjs"))) { + await import(pathToFileURL(path.join(ORIGINALS, f)).href); + } +} + +// the two sides' names, and the draft ids every roundtrip draws anew +const norm = (x) => String(x ?? "").replace(/ZABAP_SMP_APP_/g, "Z2UI5_CL_SMP_APP_") + .replace(/zabap_smp_app_/g, "z2ui5_cl_smp_app_").replace(/[0-9A-F]{32}/g, ""); +const views = (r) => actions(r).filter((a) => a[0] === "VIEW_SLOTS").map((a) => norm(JSON.stringify(a))); +const others = (r) => norm(JSON.stringify(actions(r).filter((a) => a[0] !== "VIEW_SLOTS"))); +/** the events a response's views wire: .eB(['NAME', …]) with their literal arguments */ +const wired = (r) => { + const out = new Map(); + for (const v of actions(r).filter((a) => a[0] === "VIEW_SLOTS")) { + for (const m of String(v[3] ?? "").matchAll(/\.eB\(\[([^\]]*)\]/g)) { + const [name, ...rest] = m[1].split(",").map((x) => x.trim()); + const event = name?.replace(/^'|'$/g, ""); + if (event && !out.has(event)) out.set(event, rest.map((x) => (/^'.*'$/.test(x) ? x.slice(1, -1) : "x"))); + } + } + return out; +}; + +function differences(app, label, a, j) { + const found = []; + if (a.status !== j.status) found.push(`HTTP ${a.status} vs ${j.status}`); + if (JSON.stringify(views(a)) !== JSON.stringify(views(j))) { + const la = views(a).join("\n").split(/(?=<)/); + const lj = views(j).join("\n").split(/(?=<)/); + const i = la.findIndex((x, k) => x !== lj[k]); + found.push(`view: ${la[i]?.slice(0, 200)} ≠ ${lj[i]?.slice(0, 200)}`); + } + const ma = a.json?.MODEL ?? {}; + const mj = j.json?.MODEL ?? {}; + for (const k of Object.keys(ma)) { + if ((KNOWN[app] ?? []).includes(k)) continue; + if (norm(JSON.stringify(ma[k])) !== norm(JSON.stringify(mj[k]))) found.push(`model ${k}: ${JSON.stringify(ma[k])} ≠ ${JSON.stringify(mj[k])}`); + } + if (others(a) !== others(j)) found.push(`actions: ${others(a).slice(0, 200)} ≠ ${others(j).slice(0, 200)}`); + if (norm(a.app) !== norm(j.app)) found.push(`app: ${a.app} ≠ ${j.app}`); + return found.map((f) => `${app} ${label}: ${f}`); +} + +test("every sample does what its ABAP original does, roundtrip for roundtrip", { timeout: 1_800_000 }, async (t) => { + await loadOriginals(); + const P = (o) => post(s.url, o); + const found = []; + let roundtrips = 0; + for (const app of SAMPLES) { + const original = app.replace("Z2UI5_CL_SMP_APP_", "ZABAP_SMP_APP_"); + const a = await P({ app: original }); + const j = await P({ app }); + found.push(...differences(app, "start", a, j)); + roundtrips++; + for (const [event, args] of wired(a)) { + const a0 = await P({ app: original }); + const j0 = await P({ app }); + const a1 = await P({ app: a0.app, id: a0.id, event, args }); + const j1 = await P({ app: j0.app, id: j0.id, event, args }); + found.push(...differences(app, `event ${event}`, a1, j1)); + roundtrips++; + } + } + assert.deepEqual(found, [], `${found.length} difference(s) in ${roundtrips} roundtrips`); + t.diagnostic(`${SAMPLES.length} samples, ${roundtrips} roundtrips, no difference`); +}); diff --git a/test/samples.test.mjs b/test/samples.test.mjs index 9d7721e..661fde7 100644 --- a/test/samples.test.mjs +++ b/test/samples.test.mjs @@ -19,15 +19,25 @@ const APPS = fs.readdirSync(path.join(ROOT, "srv/apps")) .filter((f) => /^z2ui5_cl_smp_app_\d+\.js$/.test(f)) .map((f) => f.replace(/\.js$/, "").toUpperCase()); -test("every sample starts, and its view is well-formed XML titled cap2UI5", async () => { +// A sample whose ABAP original shows nothing on its first roundtrip shows +// nothing here either - the translation keeps what the original does. +const NO_VIEW_ON_START = { + Z2UI5_CL_SMP_APP_186: "its init branch fills the fields and displays no view, in ABAP as here", +}; + +test("every sample starts, and its view is well-formed XML titled as in abap2UI5", async () => { assert.ok(APPS.length > 0); for (const app of APPS) { const r = await P({ app }); ok(r); const xml = slot(r, "MAIN"); + if (NO_VIEW_ON_START[app]) { + assert.equal(xml, undefined, `${app}: ${NO_VIEW_ON_START[app]} - it shows one now, so drop it from the list`); + continue; + } assert.ok(xml, `${app}: no MAIN view on the start`); assert.equal(wellFormed(xml), true, `${app}: ${wellFormed(xml)}`); - assert.match(xml, /title="cap2UI5 - /, `${app}: the page title`); + assert.match(xml, /title="abap2UI5 - /, `${app}: the page title - texts are the original's, 1:1`); assert.match(xml, /showNavButton="false"/, `${app}: started directly, there is nothing to go back to`); assert.ok(firedBy(xml, "navButtonPress"), `${app}: the back button carries the nav-back wire`); }