diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3d59588..5d2a298 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,13 +1,45 @@ +# Why this file exists, and what it has to keep moving. +# +# Every workflow in this repository pins its actions to a commit SHA rather +# than to `@v7`. A tag is a pointer its owner can move, so a job pinned to one +# runs whatever it points at that day; a commit cannot change under you. The +# cost of that safety is that nothing bumps a pin on its own — a pinned action +# quietly becomes a stale action, which is exactly what happened here: the +# deploy workflow sat two majors behind the rest of the organisation until +# somebody read the two files side by side. This is the PR that says so. +# +# Two ecosystems, matching the rest of the organisation: +# +# npm vitepress. It builds the site, and the build is half of +# `npm run check` — the gate every pull request passes. +# github-actions the deploy and check workflows. Dependabot updates a SHA +# pin in place and keeps the version comment right, so pinned +# does not become stale. version: 2 updates: - # The other repos in the ecosystem pin their actions to commit SHAs, which - # means nothing bumps them without a PR — this is that PR. - - package-ecosystem: github-actions - directory: / + - package-ecosystem: npm + directory: "/" schedule: interval: weekly + day: monday + time: "06:23" + timezone: Etc/UTC + open-pull-requests-limit: 5 + groups: + # One PR for the toolchain rather than one per package: they move + # together, and what has to be verified is a site that still builds, not + # each package alone. + toolchain: + patterns: ["*"] - - package-ecosystem: npm - directory: / + - package-ecosystem: github-actions + directory: "/" schedule: interval: weekly + day: monday + time: "06:23" + timezone: Etc/UTC + open-pull-requests-limit: 5 + groups: + actions: + patterns: ["*"] diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..a280ede --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,56 @@ +# What a pull request against this documentation gets checked for. +# +# There was nothing before this. verify-refs existed, but it ran only inside +# deploy.yml — which runs on push to main, so the one gate that decides whether +# the prose is still true about the code first spoke AFTER the merge, on the +# published site. That is how thirteen pages could go on teaching a class the +# framework had removed: nothing failed, because nothing ran in time. +# +# The job runs `npm run check` — the same command AGENTS.md tells a contributor +# to run locally, so a green machine and a green CI mean the same thing. +name: check + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: check-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + # The documented target. verify-refs checks every path, class, sample id + # and `require("abap2UI5/…")` on this site against a real checkout, so + # without it the run proves only that the site builds — it skips itself + # and exits 0 when the checkout is missing, which is exactly the silent + # pass this job exists to prevent. + - name: Check out cap2UI5 (reference target) + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: cap2UI5/cap2UI5 + ref: main + path: .cap2ui5-ref + fetch-depth: 1 + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22' + cache: 'npm' + - run: npm ci + + # verify-refs + vitepress build, in that order: a dead reference is + # reported even when the site would have built fine around it. + - name: npm run check + run: npm run check + env: + CAP2UI5_DIR: .cap2ui5-ref diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index fd420b0..926dd2b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -19,42 +19,42 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Setup Node - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 cache: npm - name: Setup Pages - uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - name: Install dependencies run: npm ci # The docs make concrete claims about the cap2UI5 repository — paths, - # class names, sample ids. Nothing else checks them: `vitepress build` - # validates internal links and stops there, so every rename upstream - # silently rots the prose. Verify against the real app. + # class names, sample ids, imports. Nothing else checks them: + # `vitepress build` validates internal links and stops there, so every + # rename upstream silently rots the prose. Verify against the real app. - name: Check out cap2UI5 (reference target) - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: cap2UI5/cap2UI5 path: .cap2ui5-ref - - name: Verify documented references - run: node scripts/verify-refs.mjs + # verify-refs + vitepress build, through the same `npm run check` a + # contributor runs locally and check.yml runs on the pull request — one + # command, so the three cannot drift into checking different things. + - name: Check (verify-refs + build) + run: npm run check env: CAP2UI5_DIR: .cap2ui5-ref - - name: Build with VitePress - run: npm run docs:build - - name: Upload artifact - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: docs/.vitepress/dist @@ -67,4 +67,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/AGENTS.md b/AGENTS.md index c71ff59..8e14c6c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,12 +8,19 @@ The VitePress documentation site for cap2UI5 (`docs/` holds the content, `docs/.vitepress/config.mjs` the nav/sidebar). Build locally with `npm ci && npx vitepress build docs`; dev server via `npx vitepress dev docs`. -Before committing, run `npm run check` — that is `verify-refs` (every path, -class and `?app_start=` named in the prose must resolve in a real cap2UI5 -checkout, and every internal anchor must exist) followed by the VitePress -build. The verifier needs a checkout: `CAP2UI5_DIR=/path/to/cap2UI5`, or a -sibling clone. It skips itself when there is none, so a green run without a -checkout proves only that the site builds. +Before committing, run `npm run check` — that is `verify-refs` followed by the +VitePress build. It is also what CI runs, on every pull request +(`.github/workflows/check.yml`) and on deploy. verify-refs checks that + +- every path, class and `?app_start=` named in the prose resolves in a real + cap2UI5 checkout, +- every `require("abap2UI5/…")` **inside a code fence** resolves through the + exports map of `core/package.json` and onto a file that exists, +- every internal anchor exists. + +The verifier needs a checkout: `CAP2UI5_DIR=/path/to/cap2UI5`, or a sibling +clone. It skips itself when there is none, so a green run without a checkout +proves only that the site builds. Exceptions — placeholder class names, paths in other repos — go in `docs/.verify-refs-ignore`, **with a reason**. An unexplained entry there is @@ -34,9 +41,16 @@ against the repos, don't guess): Path conventions inside the app repo: -- framework classes: `core/srv/z2ui5/` (layers `00/` utils, `01/` core - plumbing — including the shipped apps in `01/04/` since the 2026-08 - upstream rename, `02/` public API, `99/` add-ons like the pop helpers) +- framework classes: `core/srv/z2ui5/` — exactly three layers: `00/` utils, + `01/` core plumbing (including the shipped apps in `01/04/` since the + 2026-08 upstream rename) and `02/` public API. There is no `99/`: upstream's + frozen legacy package is deliberately not carried into the port, so + `z2ui5_cl_xml_view`, `z2ui5_cl_xml_view_cc` and the `z2ui5_cl_pop_*` popups + do not exist here. The one view builder is `z2ui5_cl_ui5_view_builder`. +- the vendored release is pinned: `z2ui5_if_app.version` says which one + (1.142.0 today). On it `_bind` is one-way and `_bind_edit` two-way — + upstream merged the two in 1.143.0, so upstream material can disagree with + what this core does. - bundled demo samples (pipeline-owned, flat): `core/srv/app/samples/` - user apps: `srv/app/` (or any folder via `Z2UI5_APP_DIRS` / `require("abap2UI5/register-apps")(dir)`) diff --git a/README.md b/README.md index a09593b..535eeb9 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ # cap2UI5 — Documentation -VitePress documentation for [**cap2UI5**](https://github.com/cap2UI5/cap2UI5) — the CAP / Node.js port of the [abap2UI5](https://github.com/abap2UI5/abap2UI5) concept. A zero-install playground of the framework runs at [cap2ui5.github.io/web-cap2UI5-build](https://cap2ui5.github.io/web-cap2UI5-build/) (built by [builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) into [web-cap2UI5-build](https://github.com/cap2UI5/web-cap2UI5-build)). +VitePress documentation for [**cap2UI5**](https://github.com/cap2UI5/cap2UI5) — the CAP / Node.js port of the [abap2UI5](https://github.com/abap2UI5/abap2UI5) concept. Published at **[cap2ui5.github.io/docs](https://cap2ui5.github.io/docs/)**. + +A zero-install playground of the framework runs at [cap2ui5.github.io/web-cap2UI5-build](https://cap2ui5.github.io/web-cap2UI5-build/) (built by [builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) into [web-cap2UI5-build](https://github.com/cap2UI5/web-cap2UI5-build)). ## Develop locally ```bash -npm install +npm ci npm run docs:dev ``` @@ -18,22 +20,23 @@ npm run docs:build # → docs/.vitepress/dist npm run docs:preview # → preview server ``` -## Structure +## Check before you commit +```bash +CAP2UI5_DIR=/path/to/cap2UI5 npm run check ``` -. -├── docs/ -│ ├── .vitepress/ -│ │ ├── config.mjs # VitePress configuration -│ │ └── theme/ # Custom theme (red brand color) -│ ├── index.md # Landing page -│ ├── guide/ # Concepts, quickstart, lifecycle, bindings, … -│ ├── examples/ # End-to-end example apps -│ ├── api/ # API reference (client, View Builder, App Interface) -│ └── reference/ # Architecture, protocol, DB, deployment -├── package.json -└── README.md -``` + +`npm run check` is `verify-refs` followed by the VitePress build, and it is exactly what CI runs — on every pull request (`.github/workflows/check.yml`) and again on deploy. It is the only gate this repository has on whether the prose is still true about the code: + +- every path, class and `?app_start=` named in the docs must exist in a real [cap2UI5](https://github.com/cap2UI5/cap2UI5) checkout, +- every `require("abap2UI5/…")` in a code example must resolve through the exports map of `core/package.json`, +- every internal anchor must exist. + +`verify-refs` needs that checkout — pass `CAP2UI5_DIR`, or clone cap2UI5 next to this repository. **Without one it skips itself and exits 0**, so a green run with no checkout proves only that the site builds. Deliberate exceptions live in `docs/.verify-refs-ignore`, each with a reason. + +## Structure + +The folder scheme, the ground truth about the cap2UI5 repo layout and the rules for linking into it are in **[AGENTS.md](AGENTS.md)** — read it before making any change. ## License diff --git a/docs/.verify-refs-ignore b/docs/.verify-refs-ignore index 422542d..c9d2a68 100644 --- a/docs/.verify-refs-ignore +++ b/docs/.verify-refs-ignore @@ -18,4 +18,13 @@ app/customer-list # a hypothetical Fiori elements app, used for contrast z2ui5_cl_app_xyz # stands for "your app class" in the interface description my_first_app # the class the getting-started walkthrough has you write my_app # same, in the why-cap2ui5 pitch +my_app_name # stands for "your class" in the ?app_start= URL on the navigation page ClassName # literal placeholder in the URL-parameter description + +# --- classes that exist UPSTREAM and deliberately not here ----------------- +# cap2UI5 does not carry abap2UI5's frozen src/99 (see guide/vs-abap2ui5). The +# names are named there precisely to say they are absent, so the checker must +# not read the mention as a claim that they exist. +z2ui5_cl_xml_view # upstream's retired view builder — replaced here by z2ui5_cl_ui5_view_builder +z2ui5_cl_xml_view_cc # its custom-control decorator, retired with it +z2ui5_cl_pop_bal # one of upstream's frozen built-in popups, superseded by the popups add-on diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index a8800cf..f252efe 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -1,5 +1,12 @@ import { defineConfig } from 'vitepress' +// Where the site is actually served from. Link previews (LinkedIn, Slack, +// WhatsApp, X) only accept ABSOLUTE urls in og:image — a relative +// "/docs/logo.jpeg" is silently dropped and the preview falls back to the +// grey placeholder card. +const SITE_URL = 'https://cap2ui5.github.io/docs' +const OG_IMAGE = `${SITE_URL}/logo.jpeg` + export default defineConfig({ title: 'cap2UI5', description: 'Bringing the abap2UI5 concept to CAP / Node.js — server-driven UI5 apps written in pure JavaScript', @@ -20,10 +27,24 @@ export default defineConfig({ }, head: [ + ['link', { rel: 'icon', type: 'image/jpeg', href: '/docs/logo.jpeg' }], + ['link', { rel: 'apple-touch-icon', href: '/docs/logo.jpeg' }], ['meta', { name: 'theme-color', content: '#d03c4a' }], ['meta', { property: 'og:type', content: 'website' }], + ['meta', { property: 'og:site_name', content: 'cap2UI5' }], + ['meta', { property: 'og:url', content: `${SITE_URL}/` }], ['meta', { property: 'og:title', content: 'cap2UI5 — Server-driven UI5 for CAP' }], - ['meta', { property: 'og:description', content: 'Build full UI5 applications from your CAP backend in JavaScript — no separate frontend project, no XML hand-crafting.' }] + ['meta', { property: 'og:description', content: 'Build full UI5 applications from your CAP backend in JavaScript — no separate frontend project, no XML hand-crafting.' }], + ['meta', { property: 'og:image', content: OG_IMAGE }], + ['meta', { property: 'og:image:type', content: 'image/jpeg' }], + // The logo is square (790x790), so the preview is a thumbnail card, not a + // wide banner — declaring the real size is what keeps it from being + // cropped. A 1200x630 banner would earn the large card; there is none yet. + ['meta', { property: 'og:image:width', content: '790' }], + ['meta', { property: 'og:image:height', content: '790' }], + ['meta', { property: 'og:image:alt', content: 'cap2UI5 — server-driven UI5 for CAP' }], + ['meta', { name: 'twitter:card', content: 'summary' }], + ['meta', { name: 'twitter:image', content: OG_IMAGE }] ], themeConfig: { @@ -52,9 +73,6 @@ export default defineConfig({ text: 'Getting Started', items: [ { text: 'What is cap2UI5?', link: '/guide/what-is-cap2ui5' }, - { text: 'Server-Driven UI, Explained', link: '/guide/server-driven-ui' }, - { text: 'Where cap2UI5 Comes From', link: '/guide/where-it-comes-from' }, - { text: 'The Ecosystem', link: '/guide/ecosystem' }, { text: 'Why cap2UI5?', link: '/guide/why-cap2ui5' }, { text: 'Try It in the Browser', link: '/guide/playground' }, { text: 'Quickstart', link: '/guide/getting-started' }, @@ -86,6 +104,13 @@ export default defineConfig({ { text: 'cap2UI5 vs. Fiori Elements', link: '/guide/vs-fiori-elements' }, { text: 'cap2UI5 vs. abap2UI5', link: '/guide/vs-abap2ui5' } ] + }, + { + text: 'Background', + items: [ + { text: 'Where cap2UI5 Comes From', link: '/guide/where-it-comes-from' }, + { text: 'The Ecosystem', link: '/guide/ecosystem' } + ] } ], diff --git a/docs/api/client.md b/docs/api/client.md index f59d2fb..8bec161 100644 --- a/docs/api/client.md +++ b/docs/api/client.md @@ -17,13 +17,22 @@ The `client` object is the only interface your app has to the outside world duri | Method | Returns | Description | |---|---|---| -| `_bind(value, opts?)` | `string` | One-way binding → `{/path}` | -| `_bind_edit(value, opts?)` | `string` | Two-way binding → `{/XX/path}` | -| `_bind_local(value)` | `string` | Local binding without an app property | +| `_bind(value, opts?)` | `string` | One-way binding → `{/PATH}` | +| `_bind_edit(value, opts?)` | `string` | Two-way binding → `{/XX/PATH}` | +| `_bind_local(value)` | `string` | Local binding without an app property → `{/__local_N}` | + +Paths are uppercased (`this.user_name` → `/XX/USER_NAME`) and mapped back onto the real property case-insensitively when the delta returns. + +::: info The two are one method upstream — but not here +On the framework release cap2UI5 pins (**1.142.0**, `z2ui5_if_app.version`) these are genuinely two bindings: `_bind` writes into the model root and is read-only on the frontend, `_bind_edit` writes into the `XX` namespace the frontend can write back through. That is what `z2ui5_cl_ui5_srv_bind` does in the shipped code, and what this documentation describes throughout. + +abap2UI5 resolved the split in **1.143.0**: there `_bind_edit` is an alias of `_bind`, and the reverse-formatter options are accepted but ignored. If you are reading upstream material, that is the difference you are looking at. → [cap2UI5 vs. abap2UI5](../guide/vs-abap2ui5) +::: **`opts`** for `_bind` / `_bind_edit`: - `path: true` → returns the bare path without `{...}` - `path: "name"` → explicit path, no reference lookup +- `name: "s_screen-city"` → resolve a member inside a bound structure by name - `custom_mapper: ".fmt"` → formatter function name - `custom_mapper_back: ".fmtBack"` → reverse formatter (only `_bind_edit`) - `custom_filter: ".f"` / `custom_filter_back: ".fb"` → aliases diff --git a/docs/examples/external-odata.md b/docs/examples/external-odata.md index 3dd753d..298d7be 100644 --- a/docs/examples/external-odata.md +++ b/docs/examples/external-odata.md @@ -28,9 +28,9 @@ The CSN model is generated as usual with `cds import https://services.odata.org/ ```js // srv/app/read_odata.js -const cds = require("@sap/cds"); -const z2ui5_if_app = require("abap2UI5/z2ui5_if_app"); -const z2ui5_cl_xml_view = require("abap2UI5/z2ui5_cl_xml_view"); +const cds = require("@sap/cds"); +const z2ui5_if_app = require("abap2UI5/z2ui5_if_app"); +const z2ui5_cl_ui5_view_builder = require("abap2UI5/z2ui5_cl_ui5_view_builder"); class read_odata extends z2ui5_if_app { @@ -42,19 +42,25 @@ class read_odata extends z2ui5_if_app { const northwind = await cds.connect.to("northwind"); this.customers = await northwind.run(SELECT.from("Customers").limit(50)); - const view = z2ui5_cl_xml_view.factory(); - const page = view.Page({ title: "Northwind - Customers" }); + const view = z2ui5_cl_ui5_view_builder.factory() + .ele({ n: `View`, ns: `mvc` }) + .a({ n: `xmlns`, v: `sap.m` }) + .a({ n: `xmlns:mvc`, v: `sap.ui.core.mvc` }); - const tab = page.Table({ items: client._bind_edit(this.customers) }); - const cols = tab.columns(); - cols.Column().Text({ text: "CompanyName" }); - cols.Column().Text({ text: "ContactName" }); - cols.Column().Text({ text: "Country" }); + const tab = view.ele(`Shell`).ele(`Page`) + .a({ n: `title`, v: `Northwind - Customers` }) + .ele(`Table`) + .a({ n: `items`, v: client._bind_edit(this.customers) }); - tab.items().ColumnListItem().cells() - .Input({ value: "{CompanyName}", enabled: true }) - .Input({ value: "{ContactName}", enabled: true }) - .Text({ text: "{Country}" }); + const cols = tab.ele(`columns`); + cols.ele(`Column`).tag(`Text`).a({ n: `text`, v: `CompanyName` }); + cols.ele(`Column`).tag(`Text`).a({ n: `text`, v: `ContactName` }); + cols.ele(`Column`).tag(`Text`).a({ n: `text`, v: `Country` }); + + tab.ele(`items`).ele(`ColumnListItem`).ele(`cells`) + .tag(`Input`).a({ n: `value`, v: `{COMPANYNAME}` }).a({ n: `enabled`, b: true }) + .tag(`Input`).a({ n: `value`, v: `{CONTACTNAME}` }).a({ n: `enabled`, b: true }) + .tag(`Text`).a({ n: `text`, v: `{COUNTRY}` }); client.view_display(view.stringify()); @@ -83,12 +89,13 @@ The same code you would write in a `srv/z2ui5-service.js` handler. **Anything No ### 2. Two-way on a list ```js -.Table({ items: client._bind_edit(this.customers) }) -.items().ColumnListItem().cells() - .Input({ value: "{CompanyName}", enabled: true }) +const tab = page.ele(`Table`).a({ n: `items`, v: client._bind_edit(this.customers) }); + +tab.ele(`items`).ele(`ColumnListItem`).ele(`cells`) + .tag(`Input`).a({ n: `value`, v: `{COMPANYNAME}` }).a({ n: `enabled`, b: true }); ``` -Because the array is two-way bound via `_bind_edit`, UI5 writes user edits **on every item property** back into the XX delta. On the next roundtrip `this.customers` holds the modified state. +Because the array is two-way bound via `_bind_edit`, UI5 writes user edits **on every item property** back into the XX delta. On the next roundtrip `this.customers` holds the modified state. The item-relative paths are uppercase (`{COMPANYNAME}`, not `{CompanyName}`): the model is written with uppercase names and mapped back onto your properties case-insensitively. ### 3. Persistence caveat @@ -104,11 +111,16 @@ async main(client) { if (client.check_on_init()) { client.set_odata_model("/odata/v4/admin/NorthwindCustomers"); - const view = z2ui5_cl_xml_view.factory(); - view.Page({ title: "Customers (OData)" }) - .Table({ items: "{/NorthwindCustomers}" }) - // ↑ no _bind_edit, but a static OData path binding - .columns().Column().Text({ text: "Company" }); + const view = z2ui5_cl_ui5_view_builder.factory() + .ele({ n: `View`, ns: `mvc` }) + .a({ n: `xmlns`, v: `sap.m` }) + .a({ n: `xmlns:mvc`, v: `sap.ui.core.mvc` }); + + view.ele(`Shell`).ele(`Page`).a({ n: `title`, v: `Customers (OData)` }) + .ele(`Table`) + .a({ n: `items`, v: `{/NorthwindCustomers}` }) + // ↑ no _bind_edit, but a static OData path binding + .ele(`columns`).ele(`Column`).tag(`Text`).a({ n: `text`, v: `Company` }); client.view_display(view.stringify()); } diff --git a/docs/examples/hello-world.md b/docs/examples/hello-world.md index 3b67645..650c6ad 100644 --- a/docs/examples/hello-world.md +++ b/docs/examples/hello-world.md @@ -4,31 +4,46 @@ The simplest variation of a cap2UI5 app: an input field, a button, a confirmatio ## Code +This is `core/srv/z2ui5/01/04/z2ui5_cl_ui5_app_hi_world.js`, the app the framework ships and the one `?app_start=z2ui5_cl_ui5_app_hi_world` starts — quoted as it stands: + ```js -// core/srv/z2ui5/01/04/z2ui5_cl_ui5_app_hi_world.js (shipped with the framework) -const z2ui5_if_app = require("abap2UI5/z2ui5_if_app"); -const z2ui5_cl_xml_view = require("abap2UI5/z2ui5_cl_xml_view"); +const z2ui5_cl_ui5_view_builder = require("../../02/z2ui5_cl_ui5_view_builder"); +const z2ui5_if_app = require("../../02/z2ui5_if_app"); class z2ui5_cl_ui5_app_hi_world extends z2ui5_if_app { - - name = ""; + name = ``; async main(client) { - if (client.check_on_init()) { - const view = z2ui5_cl_xml_view.factory() - .Shell() - .Page({ title: "abap2UI5 - Hello World" }) - .SimpleForm({ editable: true }) - .content() - .Title({ text: "Make an input here and send it to the server..." }) - .Label({ text: "Name" }) - .Input({ value: client._bind_edit(this.name) }) - .Button({ text: "Send", press: client._event("BUTTON_POST") }); + const view = z2ui5_cl_ui5_view_builder.factory() + .ele({ n: `View`, ns: `mvc` }) + .a({ n: `xmlns`, v: `sap.m` }) + .a({ n: `xmlns:mvc`, v: `sap.ui.core.mvc` }) + .a({ n: `xmlns:core`, v: `sap.ui.core` }) + // SimpleForm and its content aggregation live in sap.ui.layout.form + .a({ n: `xmlns:form`, v: `sap.ui.layout.form` }); + + const form = view + .ele({ n: `Shell` }) + .ele({ n: `Page` }) + .a({ n: `title`, v: `abap2UI5 - Hello World` }) + .ele({ n: `SimpleForm`, ns: `form` }) + .a({ n: `editable`, b: true }) + .ele({ n: `content`, ns: `form` }); + + form + .tag({ n: `Title`, ns: `core` }) + .a({ n: `text`, v: `Enter a value and send it to the server...` }) + .tag({ n: `Label` }) + .a({ n: `text`, v: `Name` }) + .tag({ n: `Input` }) + .a({ n: `value`, v: client._bind_edit(this.name) }) + .tag({ n: `Button` }) + .a({ n: `text`, v: `Send` }) + .a({ n: `press`, v: client._event(`BUTTON_POST`) }); client.view_display(view.stringify()); - - } else if (client.check_on_event("BUTTON_POST")) { + } else if (client.check_on_event(`BUTTON_POST`)) { client.message_box_display(`Your name is ${this.name}`); } } @@ -37,6 +52,41 @@ class z2ui5_cl_ui5_app_hi_world extends z2ui5_if_app { module.exports = z2ui5_cl_ui5_app_hi_world; ``` +::: tip The two import lines are the one thing you write differently +This file lives *inside* the core package, so it reaches its neighbours by relative path. Your own app sits outside the package and imports through its exports map: + +```js +const z2ui5_cl_ui5_view_builder = require("abap2UI5/z2ui5_cl_ui5_view_builder"); +const z2ui5_if_app = require("abap2UI5/z2ui5_if_app"); +``` + +Everything below those two lines is identical. +::: + +## The view it renders + +```xml + + + + + + +