Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,50 @@ jobs:
path: .samples
fetch-depth: 1

# The other two catalogues, for the counts on resources/samples.md. Only
# SAMPLES.md is needed, so only SAMPLES.md is fetched — and an unreachable
# repository costs one verified figure, never the run: check:counts skips
# what it cannot read and says which ones those were.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
continue-on-error: true
with:
repository: abap2UI5/samples-controls
ref: main
path: .samples-controls
fetch-depth: 1
sparse-checkout: SAMPLES.md
sparse-checkout-cone-mode: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
continue-on-error: true
with:
repository: abap2UI5/samples-stack
ref: main
path: .samples-stack
fetch-depth: 1
sparse-checkout: SAMPLES.md
sparse-checkout-cone-mode: false

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: 'npm'
- run: npm ci

# the catalogue parser, against a row of every shape the three sample
# repositories generate. `npm run check` has run this since the parser
# stopped matching for the second time; CI did not, so the one check that
# exists BECAUSE something broke twice in silence was the only one no pull
# request had to pass. Seconds, and no network.
- name: unit tests
run: npm test

# the release number in the nav bar, the deprecations page and the
# changelog, against the newest release tag of the framework. This one
# goes stale WITHOUT anybody touching this repository - a release happens
# over there - which is why it runs first: it is the check most likely to
# be true yesterday and false today.
# over there - which is why it runs before the build: it is the check most
# likely to be true yesterday and false today.
- name: release version
if: ${{ !cancelled() }}
run: npm run check:version

# a page that does not build is a page nobody can read
Expand All @@ -68,3 +100,11 @@ jobs:
- name: sample links
if: ${{ !cancelled() }}
run: npm run check:samples

# the four figures on resources/samples.md - three per-repository counts
# and the total they add up to - against the catalogues themselves. The
# generated llms.txt leaves a number out when it cannot count it; a prose
# page cannot, so the number stays and this is what holds it
- name: corpus counts
if: ${{ !cancelled() }}
run: npm run check:counts
27 changes: 27 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,33 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # Not needed if lastUpdated is not enabled

# The sample catalogues, for the corpus sizes in the generated llms.txt -
# one file out of each repository, and nothing else. `continue-on-error`
# is the point: generate-llms.mjs leaves the number out when a catalogue
# is not here, so a repository that is unreachable costs the deploy a
# figure, never the site.
- name: Sample catalogues
continue-on-error: true
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: abap2UI5/samples
ref: main
path: .samples
fetch-depth: 1
sparse-checkout: SAMPLES.md
sparse-checkout-cone-mode: false
- name: Sample catalogues (controls)
continue-on-error: true
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: abap2UI5/samples-controls
ref: main
path: .samples-controls
fetch-depth: 1
sparse-checkout: SAMPLES.md
sparse-checkout-cone-mode: false

- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
docs/.vitepress/cache
docs/.vitepress/dist

# the sample catalogue, cloned by CI for scripts/link-samples.mjs
# the sample catalogues, cloned by CI for scripts/link-samples.mjs and for the
# corpus sizes in the generated llms.txt
.samples
.samples-controls
.samples-stack

# generated at build time by scripts/generate-llms.mjs - a projection of the
# pages next to them, so the only correct copy is the one this build made
Expand Down
23 changes: 19 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,31 @@ person reads the page. Do not put "as an AI, …" prose back into `docs/`.
| `scripts/link-samples.mjs` | Generates the *Working Samples* block on a page from its `samples:` frontmatter plus `SAMPLES.md` in an `abap2UI5/samples` checkout, and checks the link in both directions |
| `scripts/generate-llms.mjs` | Builds `llms.txt` / `llms-full.txt` / per-page markdown from the sidebar. Runs inside `docs:build`, so the deploy publishes them |
| `scripts/check-version.mjs` | The release number in the nav bar, the deprecations page and the changelog, against the newest release tag of the framework |
| `scripts/lib/catalogue.mjs` | Parses a sample catalogue's rows; pinned by `test/catalogue.test.mjs`, because it has stopped matching twice and both times answered wrongly instead of failing |

## Build & verify — run before every commit

```bash
npm run check # docs:build + check:examples + check:samples
npm run check # test + check:version + docs:build + check:examples + check:samples + check:counts
```

`.github/workflows/check.yml` runs the same six, in the same order. Keep the
two in step: a step that exists only in `package.json` is a step no pull
request has to pass, which is how `npm test` — the pin added *because* the
catalogue parser broke twice in silence — went a release without CI.

`check:samples` needs an `abap2UI5/samples` checkout — set `SAMPLES_HOME`, or
clone it as a sibling. Without one it *skips* rather than fails, so verify the
output says what you think it says. CI checks out `abap2UI5/samples@main`
explicitly for this reason.

`check:counts` reads all three catalogues the same way, and skips per
repository: with only `samples` at hand it verifies that one figure, says the
other two were not verified, and leaves the total alone (it needs all three).
CI sparse-checks out `SAMPLES.md` from `samples-controls` and `samples-stack`
so the page is fully checked; both are `continue-on-error`, because an
unreachable repository must cost a figure and not the run.

## Things that will trip you up

- **The nav bar and the sidebar contain byte-identical lines.**
Expand All @@ -39,9 +52,11 @@ explicitly for this reason.
grepping the source.
- **A fenced ABAP example is code, and it is checked.** `check:examples`
compiles it and lints the view. It also refuses `z2ui5_cl_xml_view=>` — the
frozen builder — unless the page carries the migration banner. Examples are
the most-copied ABAP in the project; that gate is the reason the pages could
be migrated at all.
frozen builder — unless the page carries the migration banner, and refuses a
chain step calling anything but `ele` / `tag` / `a` / `end` / `stringify`,
whether it is written mid-chain (`)->input( )`) or on its receiver
(`page->input( )`). Examples are the most-copied ABAP in the project; that
gate is the reason the pages could be migrated at all.
- **`llms.txt` is generated from the SIDEBAR, not from a directory walk.** A
page in no sidebar is reported as an orphan and published anyway. If you add
a page, add it to the sidebar or accept that nothing navigates to it.
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,23 @@ Every contribution makes the documentation better for the community!
```sh
npm ci
npm run docs:dev # the site, with hot reload
npm run check # what CI runs: build + ABAP examples + sample links
npm run check # what CI runs, all six steps
```

### What CI checks

A documentation repository has no compiler for its prose, but three things in
it are decidable, and all three are decided before a merge:
A documentation repository has no compiler for its prose, but six things in
it are decidable, and all six are decided before a merge — `npm run check`
and `.github/workflows/check.yml` run the same list, in the same order:

| | |
|---|---|
| `test` | the sample-catalogue parser in `scripts/lib/`, against a row of every shape the three sample repositories generate |
| `check:version` | the release number in the nav bar, the deprecations page and the changelog, against the newest release tag of the framework — this one goes stale without anybody touching this repository |
| `docs:build` | a page that does not build is a page nobody can read |
| `check:examples` | the ABAP in the fenced blocks, against the real framework: does it compile, and does the view it builds name controls and properties that exist on the UI5 floor this documentation targets |
| `check:samples` | the **Working Samples** blocks, against [abap2UI5/samples](https://github.com/abap2UI5/samples) |
| `check:counts` | the four figures on `resources/samples.md` — one count per sample repository and the total they add up to — against the catalogues themselves |

### What the site publishes for machines

Expand Down
13 changes: 13 additions & 0 deletions docs/.vitepress/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,18 @@ export default defineConfig({
items: [
{ text: "Installation", link: "/get_started/quickstart" },
{ text: "Hello World", link: "/get_started/hello_world" },
// Between the smallest app and the complete one, because that is
// where the reader's question changes from "how do I write this"
// to "where does it live". Quickstart ends with a class typed
// into a system and nothing said the next step out of it; a
// reader who stopped once the app ran met the template, the
// linter and the agent setup on no page at all.
{ text: "Your Project", link: "/get_started/project_setup" },
{ text: "Full Example", link: "/get_started/full_example" },
],
},
{ text: "Tooling", link: "/get_started/tooling" },
{ text: "Building with AI", link: "/get_started/ai" },
{ text: `What's Next?`, link: "/get_started/next" },
],
},
Expand Down Expand Up @@ -455,6 +463,11 @@ export default defineConfig({
text: "Tool",
collapsed: true,
items: [
// The project's own linter, next to the tools it borrows. Every
// other gate in this section is somebody else's; this one is
// the only thing that can read a view that does not exist until
// the app runs.
{ text: "abap2UI5-linter", link: "/technical/tools/linter" },
{ text: "abapGit", link: "/technical/tools/abapgit" },
{ text: "ajson", link: "/technical/tools/ajson" },
{ text: "S-RTTI", link: "/technical/tools/srtti" },
Expand Down
4 changes: 3 additions & 1 deletion docs/configuration/setup/style_css.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ ENDMETHOD.
In the XML view you then reference your class via the `class` property:

```abap
view->button( text = `Delete` class = `myRedButton` ).
)->tag( `Button`
)->a( n = `text` v = `Delete`
)->a( n = `class` v = `myRedButton` )
```

## When to Use Custom CSS
Expand Down
21 changes: 10 additions & 11 deletions docs/cookbook/cheat_sheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,24 @@ A one-page recap of the rules that decide whether an abap2UI5 app works or misbe
| Check the built-in popups before building a custom dialog | Roughly twenty ready-made dialogs ship with the framework — confirm, select, file up/download, ranges, PDF, … → [Built-In](/cookbook/popup_popover/built_in) |
| Use backtick string literals (`` ` ``) | Project-wide convention in the framework, the samples and this documentation; keeps ABAP string handling consistent |

::: warning `abap_false` in `_generic( )` disappears from the view
In the **fluent API** both flags work as expectedthe builder inspects the type of the value it receives and writes `true` or `false`:
::: warning An ABAP flag passed as `v` does not reach the view as a boolean
`a( )` takes **either** `v` — any string expression**or** `b`, an ABAP boolean. Only `b` converts, and it is the form to use whenever the value comes out of ABAP:

```abap
view->button( text = `Save` enabled = abap_false ). " → enabled="false"
)->tag( `Button`
)->a( n = `text` v = `Save`
)->a( n = `enabled` b = abap_false ) " → enabled="false"
```

In **`_generic( t_prop = ... )`** they do not. The property table stores values as `string`, so the boolean type is lost on the way in:
Through `v` the flag is written verbatim: `abap_true` arrives in the view as `enabled="X"` and `abap_false` as an empty value. Neither is the `true` / `false` UI5 expects, and neither is a syntax error — the view renders, with the control in the wrong state.

- `abap_true` still ends up correct — the serializer renders a value of `X` as `true`.
- `abap_false` is a blank and becomes an empty string, and properties with an empty value are dropped from the XML entirely. The attribute is never written, so a control whose UI5 default is `true` (`enabled`, `visible`, …) silently stays enabled.

Write the literal instead — it is unambiguous in both directions:
A **literal** is a string and belongs in `v`, unquoted by any flag variable:

```abap
view->_generic( name = `Button` ns = `sap.m`
t_prop = VALUE #( ( n = `text` v = `Save` )
( n = `enabled` v = `false` ) ) ).
)->a( n = `enabled` v = `false` )
```

Any expression that yields a flag works in `b`, so there is no reason to convert by hand: `` )->a( n = `visible` b = xsdbool( lines( mt_item ) > 0 ) ) ``.
:::

## Next Steps
Expand Down
54 changes: 34 additions & 20 deletions docs/cookbook/expert_more/smart_controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,21 @@ Smart controls from the `sap.ui.comp` library (SmartFilterBar, SmartTable, Smart
The `sap.ui.comp` library ships with SAPUI5 but not with OpenUI5 — apps using smart controls require a SAPUI5 bootstrap. See [UI5 Bootstrapping](/configuration/setup/ui5_bootstrapping).
:::

## Supported Controls
## Declaring the Namespaces

The XML view builder covers the `sap.ui.comp` namespaces `smartfilterbar`, `smarttable`, `smartvariants`, `smartform`, `smartfield`, `smartchart` and `navpopover`. Since smart controls are metadata-driven, the app usually carries no ABAP data at all — it switches the default model to an OData service instead (see [OData](/cookbook/expert_more/odata)):
Smart controls live in their own `sap.ui.comp` sub-namespaces, so each one you use needs its `xmlns` on the root `View` — exactly as in a hand-written UI5 view. The three below cover a list report:

```abap
DATA(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:smartFilterBar` v = `sap.ui.comp.smartfilterbar`
)->a( n = `xmlns:smartTable` v = `sap.ui.comp.smarttable`
)->a( n = `xmlns:smartVariantManagement` v = `sap.ui.comp.smartvariants` ).
```

`smartform`, `smartfield`, `smartchart` and `navpopover` are declared the same way. Since smart controls are metadata-driven, the app usually carries no ABAP data at all — it switches the default model to an OData service instead (see [OData](/cookbook/expert_more/odata)):

```abap
client->view_display( val = view->stringify( )
Expand All @@ -23,26 +35,28 @@ client->view_display( val = view->stringify( )
A page variant is one `SmartVariantManagement` that owns the persistency for the whole page; SmartFilterBar and SmartTable register with it through their `smartvariant` association, each contributing its own `persistencykey`:

```abap
page->smart_variant_management(
id = `pageVariantId`
persistencykey = `PageVariantPKey` ).

page->smart_filter_bar(
id = `smartFilterBar`
entityset = `ProductSet`
smartvariant = `pageVariantId`
persistencykey = `SmartFilterPKey` ).

page->smart_table(
id = `smartTable`
smartfilterid = `smartFilterBar`
smartvariant = `pageVariantId`
entityset = `ProductSet`
initiallyvisiblefields = `ProductID,Name,Category,Price`
usevariantmanagement = `true`
persistencykey = `SmartTablePKey` ).
page->tag( n = `SmartVariantManagement` ns = `smartVariantManagement`
)->a( n = `id` v = `pageVariantId`
)->a( n = `persistencyKey` v = `PageVariantPKey` ).

page->tag( n = `SmartFilterBar` ns = `smartFilterBar`
)->a( n = `id` v = `smartFilterBar`
)->a( n = `entitySet` v = `ProductSet`
)->a( n = `smartVariant` v = `pageVariantId`
)->a( n = `persistencyKey` v = `SmartFilterPKey` ).

page->tag( n = `SmartTable` ns = `smartTable`
)->a( n = `id` v = `smartTable`
)->a( n = `smartFilterId` v = `smartFilterBar`
)->a( n = `smartVariant` v = `pageVariantId`
)->a( n = `entitySet` v = `ProductSet`
)->a( n = `initiallyVisibleFields` v = `ProductID,Name,Category,Price`
)->a( n = `useVariantManagement` v = `true`
)->a( n = `persistencyKey` v = `SmartTablePKey` ).
```

Without an annotated `UI.LineItem` the SmartTable starts with no columns at all, so `initiallyVisibleFields` is not optional in practice — name the columns the service is meant to show.

### The `SMART_VARIANT_INIT` Handshake

In a classic UI5 app, the controller calls `initialise( )` on the variant management once the smart controls have registered. Without it, the page variant never gets a personalizable control — saving a view fails in `sap.ui.fl` and stored views are never loaded. In abap2UI5, the `smart_variant_init` frontend event performs this handshake; it waits until the smart controls have registered (which they do once their OData metadata has arrived):
Expand Down
6 changes: 3 additions & 3 deletions docs/cookbook/model/device_model.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ abap2UI5 offers two ways to access device information: directly in the view via

By default, the device model binds to the view under the name `device`. Use standard UI5 binding syntax to show device properties directly — no backend roundtrip needed:
```abap
page->input(
description = `device model - resize - width`
value = `{device>/resize/width}` ).
)->tag( `Input`
)->a( n = `description` v = `device model - resize - width`
)->a( n = `value` v = `{device>/resize/width}` )
```
For all parameters, see the [UI5 docs](https://sapui5.hana.ondemand.com/sdk/#/api/sap.ui.Device).

Expand Down
Loading