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`); }