App Lifecycle
A cap2UI5 app is a JavaScript class with a single mandatory method: async main(client). When exactly it is called, what happens the first time, what happens on subsequent roundtrips — that is the app lifecycle.
The main(client) method
class my_app extends z2ui5_if_app {
some_field = "";
async main(client) {
// called on EVERY roundtrip
}
}It is called on every roundtrip — initial load, button click, selection change, navigation, popup close, … everything goes through main(). You differentiate via the check_* methods of the client object.
The three main states
async main(client) {
if (client.check_on_init()) {
// first call of the app instance
return;
}
if (client.check_on_navigated()) {
// app was just activated after a nav_app_call/leave
return;
}
if (client.check_on_event("MY_EVENT")) {
// a specific event has occurred
return;
}
}check_on_init()
Returns true only on the first main() call of a fresh app instance. This is the moment when you:
- Set default values
- Load external data (
cds.connect.to(...)) - Render the initial view
if (client.check_on_init()) {
this.customers = await (await cds.connect.to("northwind"))
.run(SELECT.from("Customers").limit(50));
this.render(client);
return;
}Internally: check_on_init() returns true as long as oApp.check_initialized is falsy and there is no event name. After main(), the handler sets check_initialized = true, persists the instance, and returns the response. On the next roundtrip the instance is loaded from the DB — by then check_initialized is already true and check_on_init() returns false.
check_on_event(name?)
With argument: tests whether the given event is currently active.
if (client.check_on_event("BUTTON_SAVE")) {
await this.save();
}Without argument: tests whether any event is active.
if (client.check_on_event()) {
switch (client.get().EVENT) {
case "BUTTON_SAVE": /* ... */ break;
case "BUTTON_CANCEL": /* ... */ break;
}
}Which style is nicer is a matter of taste — both work identically.
check_on_navigated()
Returns true directly after a nav_app_call(...) or nav_app_leave(...). In the navigated-into app you can use it to react to the result of a popup or a sub-app:
if (client.check_on_navigated()) {
const prev = client.get_app_prev();
if (prev instanceof MyPopup && prev.result?.confirmed) {
this.selected_id = prev.result.id;
}
}→ More in Navigation.
What happens between roundtrips
The lifecycle of an app instance:
┌──────────────────────────────────────┐
│ First GET → frontend loads │
│ First POST (S_FRONT.ID = "") │
▼ │
action_factory │
→ factory_first_start (?app_start=…) │
→ factory_system_startup │
│ │
▼ │
new MyApp() │
│ │
▼ │
apply XX delta (initial = empty) │
│ │
▼ │
main(client) ← check_on_init() === true │
│ │
▼ │
check_initialized = true │
│ │
▼ │
DB.saveApp() → new UUID generated │
│ │
▼ │
Response: { S_FRONT: { ID: <uuid>, … } } │
│ │
▼ │
Browser shows view │
│ │
▼ │
User clicks button │
│ │
▼ │
POST { S_FRONT: { ID: <uuid>, EVENT, … }, XX: { … delta … } }
│ │
▼ │
action_factory → DB.loadApp(uuid) │
│ │
▼ │
deserialize → my_app instance with old state
│ │
▼ │
apply XX delta (user inputs flow in) │
│ │
▼ │
main(client) ← check_on_event(...) === true
│ │
▼ │
DB.saveApp() → new UUID │
└──────────────────────────────────────┘Important:
- The app instance only lives for one roundtrip in memory. Afterwards it is serialized.
- Fields that are JSON-serializable survive. Functions, closures, DOM refs, external connection objects → do not.
- Properties like
clientare skipped (seeSKIP_PROPSinz2ui5_cl_core_srv_draft.js) so that no cyclic graph is created.
Pattern: fields ↔ reference-equality bindings
class search_form extends z2ui5_if_app {
search = ""; // ← bind_edit finds 'search' via reference equality
results = [];
page_size = 25;
async main(client) {
if (client.check_on_init()) { /* ... */ }
if (client.check_on_event("DO_SEARCH")) {
this.results = await this.search_db(this.search, this.page_size);
}
this.render(client);
}
render(client) {
z2ui5_cl_xml_view.factory()
.Page({ title: "Search" })
.Input({ value: client._bind_edit(this.search) })
.Button({ press: client._event("DO_SEARCH") })
.Table({ items: client._bind(this.results) });
}
}Fields you expose via bindings must be direct properties of the app. _bind_edit(this.deep.path.field) does not work for deeply nested values — you would expose this.deep and bind the sub-path as a string:
const path = client._bind_edit(this.deep, { path: true });
// path = "/XX/deep"
.Input({ value: `{${path}/path/field}` })→ Full explanation in Data Binding.
Framework fields (don't use yourself)
These properties on z2ui5_if_app are reserved:
id_draft = "";
id_app = "";
check_initialized = false;
check_sticky = false;Don't override — the binding engine explicitly excludes them from the reference lookup (_FRAMEWORK_FIELDS in z2ui5_cl_core_client.js), but don't use them as your own app state either.
Stickiness (optional)
this.check_sticky = true;
client.set_session_stateful(true);Sets a sticky session — the frontend driver then serializes roundtrips more strictly back-to-back. Useful for apps with critical ordering or file upload sequences, otherwise unnecessary.
→ Continue with View Builder.