Trial && Error

Intrusive Thoughts & Lost Bits

View on GitHub
15 October 2025

Clean Code UI5

by GonzaloMB

Practical guidelines to write maintainable, testable, and scalable SAP UI5 apps. This post covers structure, naming, async patterns, bindings, fragments, formatters, error handling, performance tips, and examples in JS/TS.


Core Principles


Project Structure (Example)

webapp/
  component.js
  manifest.json
  model/
    models.js           # factory for JSONModel, i18n, device
    constants.js        # model names, routes, ids
    formatters.js       # pure formatters
    services.js         # fetch/XHR wrappers
  controller/
    Main.controller.js
  view/
    Main.view.xml
  fragments/
    UploadDialog.fragment.xml

Constants (No Magic Strings)

// webapp/model/constants.js
export const MODELS = Object.freeze({
  I18N: "i18n",
  VIEW: "view",
  ODATA: "",
});

export const IDS = Object.freeze({
  TABLE: "ordersTable",
  DIALOG_UPLOAD: "uploadDialog",
});

export const ROUTES = Object.freeze({
  LIST: "OrdersList",
  OBJECT: "OrdersObjectPage",
});

Model Factory

// webapp/model/models.js
import JSONModel from "sap/ui/model/json/JSONModel";

export function createViewModel() {
  return new JSONModel({
    busy: false,
    search: "",
    selectedCount: 0,
  });
}

Register in component.js or onInit:

// in Component or controller onInit
import { MODELS } from "../model/constants";
import { createViewModel } from "../model/models";

this.getView().setModel(createViewModel(), MODELS.VIEW);

Controller Layout (Async/Await, Helpers)

// webapp/controller/Main.controller.js
sap.ui.define(
  [
    "sap/ui/core/mvc/Controller",
    "sap/ui/core/Fragment",
    "sap/ui/core/message/MessageManager",
    "sap/ui/core/BusyIndicator",
    "../model/constants",
    "../model/services",
  ],
  function (
    Controller,
    Fragment,
    MessageManager,
    BusyIndicator,
    constants,
    services
  ) {
    "use strict";

    return Controller.extend("demo.controller.Main", {
      onInit() {
        this._mm = sap.ui.getCore().getMessageManager();
        this.getView().addDependent(
          this._mm.registerObject(this.getView(), true)
        );
      },

      async onRefresh() {
        await this._withBusy(async () => {
          await this._reloadList();
        });
      },

      onOpenUpload() {
        return Fragment.load({
          name: "demo.fragments.UploadDialog",
          controller: this,
          id: this.getView().getId(),
        }).then((d) => {
          this._upload = d;
          this.getView().addDependent(d);
          d.open();
        });
      },

      onCloseUpload() {
        this._upload?.close();
        this._upload?.destroy(true);
        this._upload = null;
      },

      // Private helpers
      async _reloadList() {
        const table = this.byId(constants.IDS.TABLE);
        await table?.getBinding("items")?.refresh();
      },

      async _withBusy(fn) {
        BusyIndicator.show(0);
        try {
          return await fn();
        } finally {
          BusyIndicator.hide();
        }
      },
    });
  }
);

Notes:


XML View: Declarative Bindings

<!-- webapp/view/Main.view.xml -->
<mvc:View xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m" controllerName="demo.controller.Main">
  <VBox width="100%">
    <Toolbar>
      <Title text="Orders" level="H2"/>
      <ToolbarSpacer/>
      <Button text="Refresh" press="onRefresh" type="Emphasized"/>
      <Button text="Upload" press="onOpenUpload"/>
    </Toolbar>
    <Table id="ordersTable" items="{ path: '/Orders' }">
      <columns>
        <Column><Text text="ID"/></Column>
        <Column><Text text="Customer"/></Column>
        <Column><Text text="Total"/></Column>
      </columns>
      <items>
        <ColumnListItem>
          <cells>
            <Text text="{ID}"/>
            <Text text="{CustomerName}"/>
            <ObjectNumber number="{path: 'Total', type: 'sap.ui.model.type.Currency', formatOptions: { showMeasure: false }}"/>
          </cells>
        </ColumnListItem>
      </items>
    </Table>
  </VBox>
  <dependents>
    <core:Fragment id="uploadFrag" type="XML" fragmentName="demo.fragments.UploadDialog" xmlns:core="sap.ui.core"/>
  </dependents>
</mvc:View>

Tips:


Formatters (Pure and Reusable)

// webapp/model/formatters.js
export function currency(value, currency = "EUR") {
  if (value == null) return "";
  return new Intl.NumberFormat(undefined, { style: "currency", currency }).format(value);
}
``+

Use in XML:

```xml
<Text text="{ parts: ['Total'], formatter: '.formatters.currency' }"/>

Keep formatters pure (no side effects) to simplify testing.


Services (HTTP/NW RFC/Backend Wrappers)

// webapp/model/services.js
export async function requestJSON(url, options = {}) {
  const res = await fetch(url, {
    headers: { "Content-Type": "application/json" },
    ...options,
  });
  const data = await res.json().catch(() => null);
  if (!res.ok) throw new Error(data?.error?.message || res.statusText);
  return data;
}

Avoid placing fetch logic within controllers; centralize it to standardize errors and headers.


Error Handling with MessageManager

try {
  await this._withBusy(() => services.requestJSON("/api/orders"));
} catch (e) {
  const msg = new sap.ui.core.message.Message({
    message: e.message,
    type: sap.ui.core.MessageType.Error,
  });
  sap.ui.getCore().getMessageManager().addMessages(msg);
}

Benefits: consistent UX for errors, support for message popover, i18n.


Fragments (Compose and Reuse)

<!-- webapp/fragments/UploadDialog.fragment.xml -->
<core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core">
  <Dialog title="Upload">
    <content>
      <FileUploader width="100%"/>
    </content>
    <endButton><Button text="Close" press="onCloseUpload"/></endButton>
  </Dialog>
  </core:FragmentDefinition>

Rules:


Testing‑Friendly Patterns


Performance Tips


TypeScript in UI5 (Optional)

// tsconfig.json (snippet)
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "strict": true,
    "types": ["@types/openui5"],
    "moduleResolution": "bundler"
  }
}

Benefits: stronger contracts for handlers/services, better editor tooling, safer refactors.


Checklist (TL;DR)


tags: ui5 - fiori - sap