From 9e683e5918f31957e6744326c8087f896a2bb1d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 07:11:20 +0000 Subject: [PATCH 1/3] Check code fences and run the gate before the merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit verify-refs skipped every fenced block as "examples, not claims" and required a backtick immediately after a class name, so the docs could import a class the framework no longer ships and nothing said a word. Fences are where an API is taught, and an import line is a claim about the package: - every require("abap2UI5/") inside a fence is resolved through the exports map of core/package.json and onto a real file - ?app_start= in a fence is checked like one in prose - the class regex matches a backticked span that STARTS with a class name, so `z2ui5_cl_xml_view.js` and `z2ui5_cl_util.register_app_dir(dir)` count - globs (`z2ui5_cl_smp_app_*`, require("abap2UI5/…")) stay shapes, not claims - exceptions still go through docs/.verify-refs-ignore, still with a reason The gate also ran too late: verify-refs lived only in deploy.yml, which runs on push to main, so it first spoke after the merge. Add check.yml on pull_request and let both workflows run `npm run check` — the same command AGENTS.md gives contributors. Co-Authored-By: Claude --- .github/workflows/check.yml | 56 ++++++++++++++++++++++++ .github/workflows/deploy.yml | 16 +++---- scripts/verify-refs.mjs | 85 ++++++++++++++++++++++++++++++++++-- 3 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/check.yml 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..5277649 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -36,23 +36,23 @@ jobs: 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 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 with: diff --git a/scripts/verify-refs.mjs b/scripts/verify-refs.mjs index 679bc32..2296368 100644 --- a/scripts/verify-refs.mjs +++ b/scripts/verify-refs.mjs @@ -23,6 +23,15 @@ * exist in a cap2UI5 checkout * 2. every `?app_start=` names a class that actually resolves * 3. every z2ui5 class named in backticks exists somewhere in the app + * 4. every `require("abap2UI5/…")` in a FENCED CODE BLOCK resolves through + * the exports map of core/package.json, and onto a file that exists + * + * Check 4 exists because the first three did not see the largest defect this + * site ever had. Fenced blocks were skipped wholesale as "examples, not + * claims" — but an example is the one claim every reader copies, and thirteen + * pages went on teaching `z2ui5_cl_xml_view` for months after the class was + * gone. A retired API is not visible in prose; it is visible in the import + * line above the example, and that line is a claim about the package. * * Usage: * CAP2UI5_DIR=/path/to/cap2UI5 node scripts/verify-refs.mjs @@ -68,8 +77,46 @@ for (const f of files) { if (b.endsWith(".js")) classes.set(b.slice(0, -3).toLowerCase(), f); } +// ---- the core package's exports map --------------------------------------- +// `require("abap2UI5/x")` does not resolve to a path, it resolves through the +// "exports" block of core/package.json — so a subpath that is not in that map +// is a broken import even when a file of that name exists somewhere in the +// tree, and a subpath that IS in the map still has to land on a real file. +const CORE_PKG = path.join(APP, "core", "package.json"); +const EXPORTS = fs.existsSync(CORE_PKG) + ? JSON.parse(fs.readFileSync(CORE_PKG, "utf8")).exports || {} + : null; + +/** + * Resolve `abap2UI5/` (or bare `abap2UI5`) the way node does: + * an exact key wins, otherwise the pattern key with the longest prefix. + * Returns the repo-relative path it lands on, or null when nothing matches. + */ +function resolveExport(subpath) { + if (!EXPORTS) return null; + const key = subpath ? `./${subpath}` : `.`; + const inApp = (target) => `core/${target.replace(/^\.\//, "")}`; + + if (typeof EXPORTS[key] === "string") return inApp(EXPORTS[key]); + + let best = null; + for (const [k, target] of Object.entries(EXPORTS)) { + const star = k.indexOf("*"); + if (star === -1 || typeof target !== "string") continue; + const pre = k.slice(0, star); + const post = k.slice(star + 1); + if (!key.startsWith(pre) || !key.endsWith(post)) continue; + if (key.length < pre.length + post.length) continue; + if (best && pre.length <= best.pre.length) continue; + best = { pre, post, target, star: key.slice(pre.length, key.length - post.length) }; + } + if (!best) return null; + return inApp(best.target.replace(`*`, best.star)); +} + if (LIST) { console.log(`${files.size} paths, ${classes.size} classes in ${APP}`); + console.log(`exports map: ${EXPORTS ? Object.keys(EXPORTS).length + " subpaths" : "NOT FOUND"}`); const samples = [...classes.keys()].filter((c) => c.startsWith("z2ui5_cl_smp_")).sort(); console.log(`samples (${samples.length}): ${samples.join(", ")}`); process.exit(0); @@ -96,7 +143,14 @@ const add = (file, line, msg) => const PATH_ROOTS = ["core/", "srv/", "db/", "app/", "test/"]; const PATH_RE = /`([A-Za-z0-9_@./-]+\/[A-Za-z0-9_@./-]+)`/g; const APP_START_RE = /app_start=([a-z0-9_]+)/gi; -const CLASS_RE = /`(z2ui5_(?:cl|if|cx)_[a-z0-9_]+)`/gi; +// A backticked span that STARTS with a z2ui5 class name. It deliberately does +// not require the closing backtick to follow the identifier: the docs write +// `z2ui5_cl_xml_view.js`, `z2ui5_cl_util.register_app_dir(dir)` and +// `z2ui5_cl_xml_view=>factory( )`, and demanding a bare identifier meant every +// one of those mentions was invisible to this checker. +const CLASS_RE = /`(z2ui5_(?:cl|if|cx)_[a-z0-9_]+)(?![a-z0-9_])/gi; +// require("abap2UI5"), require("abap2UI5/z2ui5_if_app"), … in a code fence. +const REQUIRE_RE = /require\(\s*["'`]abap2UI5(?:\/([^"'`]+))?["'`]\s*\)/g; // Tokens the docs use that are not claims about this repository — other // repos' paths, files the reader creates, placeholder class names. Each is @@ -129,14 +183,36 @@ for (const file of markdownFiles(DOCS)) { const text = fs.readFileSync(file, "utf8"); const lines = text.split("\n"); - // Fenced code blocks are examples, not claims about the repository — - // they legitimately show paths a reader will create. + // Fenced code blocks show paths a reader will create, so the path check + // stays out of them — but the imports and the `?app_start=` URLs in an + // example are claims about the package like any other, and are checked. let inFence = false; lines.forEach((line, i) => { if (/^\s*```/.test(line)) { inFence = !inFence; return; } - if (inFence) return; const n = i + 1; + if (inFence) { + for (const m of line.matchAll(REQUIRE_RE)) { + const sub = m[1] || ``; + const spec = `abap2UI5${sub ? `/${sub}` : ``}`; + if (/[*…]/.test(sub)) continue; // a shape, not an import + if (IGNORE.has(spec.toLowerCase())) continue; + if (!EXPORTS) continue; // no core package to check against + const target = resolveExport(sub); + if (!target) { + add(file, n, `require("${spec}") has no match in the core exports map`); + } else if (!files.has(target)) { + add(file, n, `require("${spec}") resolves to ${target}, which does not exist`); + } + } + for (const m of line.matchAll(APP_START_RE)) { + const cls = m[1].toLowerCase(); + if (classes.has(cls) || IGNORE.has(cls)) continue; + add(file, n, `?app_start names a class that does not exist: ${m[1]}`); + } + return; + } + for (const m of line.matchAll(PATH_RE)) { const p = m[1].replace(/^\.\//, "").replace(/\/$/, ""); if (!PATH_ROOTS.some((r) => (p + "/").startsWith(r))) continue; @@ -155,6 +231,7 @@ for (const file of markdownFiles(DOCS)) { for (const m of line.matchAll(CLASS_RE)) { const cls = m[1].toLowerCase(); + if (line[m.index + m[0].length] === "*") continue; // glob: a family, not a class if (classes.has(cls) || IGNORE.has(cls)) continue; // Interfaces are not always separate files, and abstract/ABAP-only // names appear when contrasting with abap2UI5 — only flag cl_ classes, From 4afcf50f8b0351cb2c3832ca23f9b4f1f0eab233 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 07:25:41 +0000 Subject: [PATCH 2/3] Teach the API the framework actually ships MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The retired z2ui5_cl_xml_view was still the view builder on 13 of 33 pages, including examples/hello-world.md, which named the shipped file z2ui5_cl_ui5_app_hi_world.js and then showed an API that file has not used for a long time. Every example now builds views with z2ui5_cl_ui5_view_builder, checked method by method against core/srv/z2ui5/02/z2ui5_cl_ui5_view_builder.js: factory / ele / tag / a / end / stringify, and nothing else. hello-world quotes the real file verbatim. Other claims corrected while migrating: - z2ui5_cl_xml_view.factory_popup(), .xml_load() and view._z2ui5() .approve_popover() do not exist — popups and popovers are ordinary core:FragmentDefinition trees, and a static XML snippet is spliced into the stringified view - there is no layer 99 and no 02/z2ui5_cl_pop_* in core/srv/z2ui5/ (00, 01, 02 is the whole tree), so reference/architecture.md, guide/ project-structure.md and this repo's AGENTS.md stop documenting one - model paths are uppercased (/XX/NAME, {TITLE}); the docs showed them lowercase everywhere, which is a binding that silently resolves to nothing - reference/protocol.md's sample roundtrip named a class that does not exist; it now uses the real hi_world app Binding semantics are now stated per release rather than as "1:1 identical": the pinned core is 1.142.0, where _bind is one-way and _bind_edit two-way; upstream merged the two in 1.143.0 and cap2UI5 additionally does not carry upstream's frozen legacy package at all. api/client.md, guide/vs-abap2ui5.md and the landing page say so. Guide consolidation: the pitch was spread over five pages before the Quickstart — the Fiori-Elements-to-freestyle gap in four of them, the abap2UI5 origin in four. server-driven-ui.md is merged into what-is-cap2ui5.md (which now carries the pattern, the gap and the costs); why-cap2ui5.md keeps only the project-level case; where-it-comes-from.md keeps the origin and the pipeline and moves, with ecosystem.md, into a Background section after the comparisons. Also: ecosystem.md's "six repositories" heading sat over seven rows. Co-Authored-By: Claude --- AGENTS.md | 32 ++++-- docs/.verify-refs-ignore | 9 ++ docs/.vitepress/config.mjs | 10 +- docs/api/client.md | 15 ++- docs/examples/external-odata.md | 58 ++++++----- docs/examples/hello-world.md | 88 +++++++++++++---- docs/examples/list.md | 70 ++++++------- docs/examples/selection-screen.md | 159 +++++++++++++++++++----------- docs/examples/static-xml-view.md | 15 ++- docs/guide/data-binding.md | 85 ++++++++++------ docs/guide/ecosystem.md | 13 ++- docs/guide/getting-started.md | 40 ++++---- docs/guide/lifecycle.md | 20 ++-- docs/guide/popups.md | 106 +++++++++++++------- docs/guide/project-structure.md | 27 +++-- docs/guide/server-driven-ui.md | 90 ----------------- docs/guide/vs-abap2ui5.md | 75 +++++++++----- docs/guide/what-is-cap2ui5.md | 113 +++++++++++++++------ docs/guide/where-it-comes-from.md | 15 +-- docs/guide/why-cap2ui5.md | 12 +-- docs/index.md | 2 +- docs/public/.gitkeep | 0 docs/reference/architecture.md | 21 ++-- docs/reference/protocol.md | 14 +-- 24 files changed, 641 insertions(+), 448 deletions(-) delete mode 100644 docs/guide/server-driven-ui.md delete mode 100644 docs/public/.gitkeep 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/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..907a025 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -52,9 +52,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 +83,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 + + + + + + +