sapui5tutors SAPUI5 • Fiori • SAP BTP Step-by-step tutorials Real project examples Interview Q&A
Practical SAPUI5 • Fiori • SAP BTP tutorials and interview prep
sapui5tutors

CDS Views (DDLS) in ABAP: A Practical Introduction

12:13:00

Every modern ABAP application — RAP, Fiori Elements, OData services — sits on top of CDS views. If you are coming from classic ABAP, where the data model was a pile of transparent tables and the occasional database view, CDS (Core Data Services, written in the DDLS language) can feel abstract at first. In practice, it is simpler than it looks: CDS views are just a way to define your data model in the database layer, with semantics attached.


What a CDS view actually is

A CDS view is a data definition written in DDLS (Data Definition Language Syntax) that lives in the ABAP repository and gets created as a view in the HANA database on activation. Unlike a classic SE11 database view, a CDS view carries annotations — metadata that tells consumers what the data means, not just what it contains.

Here is a minimal example:

@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Customer basic data'
define view entity ZI_Customer
  as select from zcustomer_table
{
  key customer_id   as CustomerId,
      name          as Name,
      city          as City,
      country       as Country
}

A few things to notice. The key marks the primary key. The aliases after as define the element names that consumers see. And the annotations at the top control behavior: no view enhancement, no authorization check on this basic interface view.

A CDS view is not just a SELECT statement with a name. The annotations are the point — they turn raw columns into a consumable data model.

The layered model: why there is more than one view

In real projects you rarely expose a single CDS view directly. The standard pattern has layers:

  • Interface views (like ZI_Customer above) — close to the database tables, reusable across applications. Think of them as the stable foundation.
  • Projection views — shaped for a specific consumer. They select from interface views, add UI and OData annotations, and define what a Fiori app or API actually sees.

The projection view is where annotations like @UI.lineItem or @OData.publish: true live. The interface view stays clean and reusable; the projection view is tailored. When requirements change for one app, you adjust its projection — the foundation does not move.


Annotations: where the magic happens

Annotations are the reason CDS replaced so many older techniques. A few categories you will meet constantly:

Semantics — @Semantics.amount.currencyCode: 'Currency' tells the framework that a field is a monetary amount tied to a currency field. The UI then formats it correctly without any code.

UI — @UI.lineItem: [{ position: 10 }] places a field on a Fiori Elements list report. @UI.identification puts it on the object page header. The app UI is, to a large extent, generated from these.

Consumption and OData — @OData.publish: true exposes the view as an OData service. @Search.searchable enables Fiori search on it.

Here is the customer view with UI annotations added, the way a projection view would carry them:

@EndUserText.label: 'Customer projection for sales app'
@UI.headerInfo: { typeName: 'Customer',
                  title: { value: 'Name' } }
define view entity ZC_Customer
  as projection on ZI_Customer
{
  @UI.lineItem: [{ position: 10 }]
  @UI.identification: [{ position: 10 }]
  key CustomerId,

  @UI.lineItem: [{ position: 20 }]
  Name,

  @UI.lineItem: [{ position: 30 }]
  City,

  Country
}

No UI code was written. A Fiori Elements app generated on top of this projection renders a list report with three columns and an object page header — driven entirely by annotations.


Associations: relationships without joins in every query

CDS views define relationships with association, and the framework resolves them on demand:

define view entity ZI_Order
  as select from zorder_table
  association [1..1] to ZI_Customer as _Customer
    on $projection.customer_id = _Customer.CustomerId
{
  key order_id    as OrderId,
      customer_id as CustomerId,
      total       as Total,
      _Customer
}

The _Customer association is exposed as a navigation path. Consumers expand it when they need customer data (?$expand=_Customer in OData) and ignore it when they do not. You define the relationship once, in the model, instead of rewriting the join in every report.

Define relationships once as associations in the data model. Every consumer — OData, analytics, RAP — reuses them instead of re-implementing joins.

Where CDS views sit in the stack

To place it all on one map:

Database tables (persistence)
        ↓
Interface CDS views (reusable data model, associations)
        ↓
Projection CDS views (consumer-specific shaping + annotations)
        ↓
Service definition / binding → OData → Fiori Elements app

In RAP, the behavior definition attaches to these views to add transactional behavior. In analytics, the same views feed queries. The CDS layer is the single data model everything else builds on — which is why "learn CDS properly" is the most repeated advice for ABAP developers moving to the cloud stack. It is not hype. Everything downstream depends on getting this layer right.

Read more →
sapui5tutors

Modern ABAP Syntax: VALUE, String Templates and CORRESPONDING

12:11:00

ABAP has changed more in the last decade than in the twenty years before it. If you learned ABAP in the R/3 or ECC era — DATA declarations at the top, CONCATENATE for strings, MOVE-CORRESPONDING for structure mapping — the modern syntax can look like a different language. Three constructs do most of the heavy lifting in the new style: VALUE #( ), string templates, and CORRESPONDING #( ).

Here is what each one does, with the old pattern it replaces.


VALUE #( ): inline construction

The VALUE constructor expression builds a data object — a structure, an internal table, or an elementary value — inline, in a single expression. No separate declaration, no CLEAR, no APPEND loop for simple cases.

The old way of filling an internal table:

DATA lt_customers TYPE TABLE OF ty_customer.
DATA ls_customer TYPE ty_customer.

ls_customer-id = '1001'.
ls_customer-name = 'Acme Corp'.
APPEND ls_customer TO lt_customers.

ls_customer-id = '1002'.
ls_customer-name = 'Globex Inc'.
APPEND ls_customer TO lt_customers.

The modern way:

DATA(lt_customers) = VALUE ty_customer_table(
  ( id = '1001'  name = 'Acme Corp' )
  ( id = '1002'  name = 'Globex Inc' ) ).

Each pair of parentheses is one row. The # means "infer the type from context" — here from the ty_customer_table type, or from the target variable when used in an assignment.

VALUE also replaces CREATE DATA for data references and gives you a clean way to initialize structures:

" Old: declare, then clear, then fill field by field
" New: one expression
DATA(ls_order) = VALUE ty_order( order_id = '450001'
                                status   = 'OPEN'
                                total    = '1500.00' ).
VALUE #( ) is not just shorter — it makes the data's shape visible at the point of creation. When you read the code six months later, you see the content, not the ceremony.

String templates: say goodbye to CONCATENATE

String templates embed variables and expressions directly inside a string using |...| delimiters and { } placeholders. They replace CONCATENATE and most uses of the && operator.

" Old way
CONCATENATE 'Order' lv_order_id 'has status' lv_status
  INTO lv_message SEPARATED BY space.

" New way
lv_message = |Order { lv_order_id } has status { lv_status }|.

The template is far easier to read, and it gets better: you can put full expressions inside the braces, including method calls and constructor expressions.

lv_message = |{ lines( lt_items ) } items, total { calculate_total( ) }|.
lv_label   = |Customer: { ls_customer-name WIDTH = 20 ALIGN = LEFT }|.

Formatting options like WIDTH, ALIGN, and CASE go right inside the placeholder — no more chaining TRANSLATE or padding functions afterwards. There are also dedicated template functions: substring( ), replace( ), to_upper( ), and more, all usable inline.

One caveat: string templates always produce a string of type string. If you need a fixed-length c field, assign or convert explicitly. In ABAP Cloud and RAP code, string is usually what you want anyway.


CORRESPONDING #( ): functional structure mapping

MOVE-CORRESPONDING copies components with matching names between structures. CORRESPONDING #( ) is its expression-based counterpart — it returns the mapped structure as a value, which means you can use it inside larger expressions, method calls, and constructor arguments.

" Map a database structure to a UI-friendly structure, inline
ls_display = CORRESPONDING ty_display( ls_db_record ).

Where it really shines is mapping whole tables in one line:

" Old way: LOOP ... MOVE-CORRESPONDING ... APPEND ... ENDLOOP.
" New way:
lt_display = CORRESPONDING ty_display_table( lt_db_records ).

You can also control the mapping explicitly. MAPPING renames components, EXCEPT skips them:

lt_result = CORRESPONDING ty_result_table(
  lt_source MAPPING id = customer_id
                   name = full_name
            EXCEPT internal_flag ).

And because it is an expression, it nests naturally with VALUE:

DATA(lt_ui) = VALUE ty_ui_table(
  FOR ls_db IN lt_db_records
  ( CORRESPONDING ty_ui_line( ls_db ) ) ).
CORRESPONDING #( ) turns structure mapping from a statement you write around your logic into a value you build your logic from. That shift — statements to expressions — is the heart of modern ABAP style.

Putting it together

Here is a small but realistic example — building a message list for a RAP validation, using all three constructs:

DATA(lt_messages) = VALUE ty_message_table(
  FOR ls_item IN lt_items WHERE ( quantity < 0 )
  ( id        = ls_item-id
    text      = |Item { ls_item-id }: quantity cannot be negative|
    severity  = 'E'
    details   = CORRESPONDING ty_details( ls_item ) ) ).

Inline table construction, string templates, and structure mapping in a single readable expression. That is modern ABAP: less ceremony, more signal. If you are moving to ABAP Cloud, this style is not optional decoration — it is how the language is meant to be written now, and the ATC checks will nudge you toward it.

Read more →
sapui5tutors

xs-app.json: Approuter Routing in SAP BTP

12:01:00

Every Fiori or CAP app running on SAP BTP has a small JSON file sitting at its root that most developers copy from a template and never really read. That file is xs-app.json, and it controls something fundamental: how the approuter decides where each incoming request goes, who is allowed in, and which backend system answers.

Once you understand it, a whole class of "my app works locally but not on BTP" problems starts making sense.


What the approuter does

The approuter is a Node.js reverse proxy that sits in front of your app. Every request from the browser hits the approuter first. It handles authentication (redirecting to XSUAA when needed), then forwards the request — either to your app's own static resources, or to a backend destination like a CAP service or an S/4HANA system.

xs-app.json is the approuter's rulebook. Without it, the approuter does not know your routes exist.


The anatomy of a route

Here is a typical xs-app.json for a CAP-backed Fiori app, with annotations:

{
  "welcomeFile": "/index.html",
  "authenticationMethod": "route",
  "logout": {
    "logoutEndpoint": "/do/logout"
  },
  "routes": [
    {
      "source": "^/myapp/(.*)$",
      "target": "/$1",
      "localDir": "app/myapp/webapp",
      "authenticationType": "xsuaa"
    },
    {
      "source": "^/odata/v4/catalog/(.*)$",
      "destination": "srv-api",
      "authenticationType": "xsuaa"
    },
    {
      "source": "^/api/s4/(.*)$",
      "destination": "s4hana-backend",
      "authenticationType": "xsuaa",
      "csrfProtection": false
    }
  ]
}

Let us break down what each part means.


Sources, targets, and destinations

Each route has a source — a regular expression matched against the incoming URL path. The first matching route wins, so order matters: put specific routes before generic ones.

Then there are two ways to say where the request goes:

  • localDir — serve static files from a folder inside the approuter app itself. This is how your Fiori UI5 resources get served. The target rewrites the path (here, stripping the /myapp prefix).
  • destination — forward to a named destination. The name (like srv-api or s4hana-backend) must match a destination configured in the BTP destination service or in the default-env.json for local testing.

A frequent gotcha: the destination name in xs-app.json must exactly match the destination instance name — including case. A mismatch gives you a cryptic 502 and an afternoon of debugging.

Routes are evaluated top to bottom, first match wins. Put your most specific routes first and any catch-all route last — otherwise the catch-all swallows everything below it.

Authentication per route

The authenticationType field controls whether the approuter demands a login:

  • xsuaa — the user must be authenticated via XSUAA. Unauthenticated requests get redirected to the identity provider. This is what you want for anything behind a login.
  • none — public route, no authentication. Useful for health checks or genuinely public content — but think twice before using it on API routes.

The top-level authenticationMethod field has two options: route (each route declares its own auth, as above) or all (everything requires authentication). For most apps, route gives you the flexibility you need.


A few fields people overlook

welcomeFile — what gets served when someone hits the approuter root (/). Usually your Fiori launchpad page or app index.

csrfProtection — the approuter validates CSRF tokens on state-changing requests by default. If you are proxying to a backend that handles CSRF itself (or does not use it), you may need to disable it per route, as in the S/4HANA example above. Do not disable it globally unless you understand the implications.

httpMethods — restrict a route to specific HTTP verbs. Handy when you want GETs to go to one destination and POSTs somewhere else, though that is rare in practice.


How it fits together at runtime

Picture the request flow for a Fiori app backed by CAP:

  1. Browser requests https://myapp.cfapps.../myapp/index.html.
  2. Approuter matches the first route, sees authenticationType: xsuaa, and — if no session exists — redirects to XSUAA for login.
  3. After login, the approuter serves index.html from localDir.
  4. The UI5 app then calls /odata/v4/catalog/Books. The approuter matches the second route and proxies to the srv-api destination (your CAP service), attaching the user's JWT so the backend can authorize the request.

That JWT forwarding is the quiet superpower of the approuter: your backend receives the authenticated user's identity without any extra code, which is exactly what XSUAA-based authorization in CAP relies on.

If your app works with cds watch locally but fails on BTP, check xs-app.json first: destination names, route order, and authentication types are the cause more often than not.

Local testing tip

You can run the approuter locally with npm start (the @sap/approuter package) and a default-env.json that defines your destinations. This catches route mistakes before you deploy. The destination names in default-env.json must match the ones in xs-app.json — same rule as on BTP, same 502 if you get it wrong.

Read more →
sapui5tutors

Cloud Foundry Orgs and Spaces Explained (SAP BTP)

11:59:00

If you have ever logged into the SAP BTP cockpit, opened a subaccount, and wondered where exactly your app is supposed to live, you have run into the Cloud Foundry account model. Orgs, spaces, apps, services — the hierarchy looks simple on a diagram, but the first time you deploy something, the question always comes up: do I need a new org for this, or just a space?

Let us walk through it the way it actually works in practice, not just the way the documentation draws it.


What an org actually is

An org (short for organization) is the top-level grouping inside a Cloud Foundry environment on BTP. Think of it as a department or a business unit. It does not run anything by itself — no apps, no services, no routes live directly in an org. Its job is to contain spaces and to give you a boundary for billing, quotas, and access.

In a typical BTP setup, you get one org per subaccount in the Cloud Foundry environment. Most teams never create a second org. One is enough for the vast majority of projects, because the real isolation happens one level down.

The org is a billing and access boundary. The space is where work actually happens. If you remember only one thing from this post, make it that.

Spaces: where apps and services live

A space is the unit of deployment. Every app you push, every service instance you create, every route you map — all of it belongs to a space. When you run cf push, the app lands in whatever space you are currently targeting.

The standard layout that most SAP teams settle on looks like this:

org: mycompany-prod
├── space: dev        ← developers push freely, experiments welcome
├── space: test       ← QA validates, data refreshed from prod-like sources
└── space: prod       ← locked down, deployments only via pipeline

Why separate spaces instead of separate orgs? Because spaces share the org's quota and service marketplace, which keeps things simple, while still giving you separate runtime environments, separate service instances (so your dev database is not your prod database), and separate user roles.


Roles: who can do what, and where

Cloud Foundry roles are assigned per org and per space, and the distinction matters.

Org-level roles are about administration: Org Manager (manages the org, invites users, assigns space roles), Org Auditor (read-only view of org usage), and Billing Manager (sees usage and invoices). In most SAP landscapes, only a handful of platform admins hold Org Manager.

Space-level roles are where daily life happens:

  • Space Manager — invites users to the space, manages its roles.
  • Space Developer — the workhorse role. Push apps, bind services, create routes, view logs. This is what every developer on the team needs.
  • Space Auditor — read-only. Useful for security reviewers or external auditors who need visibility without the ability to break anything.

A common mistake: giving everyone Org Manager "to make things easier." Do not do that. Org Managers can delete spaces. Space Developer in dev, Space Auditor in prod — that is the shape of a sane setup.


Services, routes, and domains per space

A few things that trip people up:

Service instances are space-scoped. When you create a destination service instance or an XSUAA instance in the dev space, it does not exist in test or prod. You create one per space, usually with the same name so your mta.yaml stays identical across landscapes.

Routes are space-scoped too. Your dev app might live at myapp-dev.cfapps.eu10.hana.ondemand.com while prod uses a custom domain. The route belongs to the space, so there is no collision.

Quotas cascade. The org has a quota (memory, service instances, routes). Spaces can have their own sub-quotas carved out of it. If your dev space keeps hitting memory limits, check the space quota before assuming the org is full — someone may have capped dev to protect prod.


A practical example: targeting and deploying

Here is the everyday flow. You log in, target the right org and space, then push:

cf login -a https://api.cf.eu10.hana.ondemand.com
cf target -o mycompany-prod -s dev
cf push myapp

The cf target step is the one people forget, and then they wonder why their app landed in the wrong space. Make it a habit to check cf target before every push — or better, let your CI/CD pipeline handle targeting so humans never have to think about it.

Keep service instance names identical across dev, test, and prod spaces. Your deployment descriptors stay the same, and promoting from one landscape to the next becomes a non-event.

When would you actually need a second org?

Rarely, but it happens. Separate orgs make sense when you need hard isolation: different cost centers that must be billed separately, a subsidiary with its own admins, or a strict regulatory boundary where even platform admins should not cross over. For 95% of SAP BTP projects, one org with well-designed spaces is the right answer.

Start with one org. Add spaces for your landscapes. Assign roles at the space level. Only reach for a second org when billing or compliance forces your hand — not before.

Read more →
sapui5tutors

SAP Fiori Launchpad: Spaces, Pages, Catalogs and Tiles Deep-Dive

11:44:00

The Fiori launchpad is the front door to S/4HANA — the shell every user passes through, the place where apps live as tiles, and the source of endless confusion about spaces, pages, catalogs, and groups. These concepts layer on top of each other, and mixing them up leads to the classic symptom: "I assigned the role but the tile doesn't show up."

Here's how the launchpad actually fits together, from the shell down to the tile.


The launchpad's role: shell, not app

The launchpad isn't an application — it's the operating shell for all Fiori apps. It provides the header bar (search, notifications, user menu, app finder), the navigation framework, and the runtime services every app relies on: intent-based navigation, personalization storage, bookmarking, and the app-to-app communication bus.

Think of it like a phone's home screen. The home screen doesn't do anything itself; it launches apps, organizes them, and provides system services (notifications, search, settings). The launchpad plays exactly that role for the enterprise: one consistent entry point, with the apps as interchangeable parts inside it.

This architecture is what makes intent-based navigation possible. Apps don't link to each other by URL — they declare intents (a semantic object plus an action, like SalesOrder-display), and the launchpad resolves each intent to the right app at runtime based on the user's roles. Swap the target app, and every link across the system follows without a single code change.

The launchpad's job: be invisible. Users should think in terms of tasks ("approve the order"), not in terms of the shell. When the launchpad draws attention to itself, something's wrong.

Spaces and pages: what the user sees

Spaces are the top-level organizing principle — one space per role or responsibility area. "Sales Manager," "Warehouse Clerk," "Finance Controller": each gets a space containing everything that role needs. Spaces appear as tabs or entries in the launchpad navigation; switching spaces switches context entirely.

Pages live inside spaces and hold the actual content: tiles, cards, and links arranged in sections. A Sales Manager space might contain pages for "Overview," "Orders," and "Analytics" — each page a curated working surface for part of the role.

The hierarchy is strict: spaces contain pages, pages contain sections, sections contain tiles/cards/links. Users can personalize within limits — rearranging, adding apps from the App Finder — but the administrator defines the structure. Personalization is stored per user; the delivered structure stays intact underneath.

This replaced the older groups model, where tiles sat in flat groups on a single home page. Groups still exist for compatibility, but spaces-and-pages is the current structure — hierarchical, role-shaped, and far better at handling the dozens of apps a real role needs.


Catalogs: what the administrator assigns

Here's the concept that causes the most confusion: a catalog is an administrative container, not something users see. Catalogs hold tiles and target mappings; administrators assign catalogs to roles (PFCG roles on-premise, business roles in cloud). Users never browse catalogs — they see the spaces and pages built from catalog content.

The flow works like this:

1. SAP delivers business catalogs — e.g., "Sales Order Processing" — containing the tiles and navigation targets for a functional area.
2. The administrator assigns business catalogs to roles. A role can reference multiple catalogs.
3. The administrator (or key user) builds spaces and pages pulling tiles from the catalogs the role can see.
4. The user opens the launchpad, sees their spaces, and launches apps. Catalogs are invisible throughout.

So when "the tile doesn't show up," the debugging chain is: is the tile in a catalog? Is that catalog assigned to the user's role? Is the tile placed on a page in one of the user's spaces? A break at any link hides the tile. Most often the culprit is step 2 — the catalog-to-role assignment — because it's the least visible link in the chain.

There's also a useful distinction between business catalogs (SAP-delivered, shouldn't be modified) and technical catalogs (the underlying target mappings and app descriptors the business catalogs reference). Custom tiles go in custom business catalogs that reference either SAP's technical catalogs or your own.

Remember: catalogs are for assignment (admin → role), spaces/pages are for presentation (role → user). Mixing up which layer you're working in is the root of most launchpad configuration headaches.

Tiles, cards, and links: the content

Tiles are the classic launchpad content — square app launchers, optionally showing live data (a KPI tile showing "47 open orders" with a trend arrow). Static tiles just launch; dynamic and KPI tiles preview information so users can decide whether launching is even necessary.

Cards are the newer integration-card format: richer than tiles, they can show lists, tables, charts, and even actions inline without opening the app. An approval card might show the three pending items with Approve/Reject buttons right on the launchpad. Cards are where the launchpad is heading — more done without leaving the home surface.

Links are the minimal form: plain text navigation entries for apps that don't merit a tile. Useful for rarely used transactions or for keeping a page compact.

All three resolve through target mappings — the technical records that bind an intent (semantic object + action) to an actual app (URL, component, parameters). The tile is the visible face; the target mapping is the wiring. When a tile is visible but clicking it fails, the target mapping is usually where to look.


Putting it together: a concrete example

A sales manager logs in. The launchpad shows their spaces: "Sales Operations," "Analytics," "Approvals." They open Sales Operations and see pages for "Orders" and "Customers." The Orders page shows KPI tiles (open orders, overdue deliveries), a card listing orders awaiting approval with inline approve buttons, and links to the full order apps.

Behind the scenes: SAP's "Sales" business catalogs are assigned to the Sales Manager business role. The administrator composed the spaces and pages from those catalogs' tiles. Each tile resolves through target mappings to Fiori apps via intents like SalesOrder-manage. The manager sees tasks; the machinery stays hidden.

When the company adds a custom "Commission Report" app, the developer creates a tile in a custom catalog, the admin assigns that catalog to the role and drops the tile onto the Analytics page. The manager finds it at next login.


Bottom line

Shell (the launchpad) → spaces (per role) → pages (working surfaces) → tiles/cards/links (content), with catalogs as the invisible assignment layer feeding roles. Learn that stack and the launchpad stops being mysterious — including the part where you debug why a tile isn't showing up.

Read more →
sapui5tutors

SAP Fiori Design Principles: Role-Based, Adaptive, Coherent, Simple, Delightful

11:43:00

Every Fiori app looks and behaves a certain way — consistent navigation, similar page structures, familiar interaction patterns. That's not an accident of shared components. It comes from five design principles that every Fiori app is supposed to embody. Whether you're building, buying, or just evaluating Fiori apps, these principles are the yardstick.

Here they are, with what each one actually demands in practice.


1. Role-based: built for the job, not the database

Traditional enterprise software exposes the system's structure: transaction codes organized by module, screens that mirror database tables. Fiori flips this. Apps are designed around roles — the purchaser, the warehouse worker, the sales manager — and each role gets exactly the apps, data, and actions their job needs.

In practice this means the launchpad shows different tiles to different users, driven by role assignments. A warehouse worker sees stock overview and goods movement apps; they never see the finance closing cockpit. It's not just hiding menu items — the apps themselves are scoped to the role's tasks, with irrelevant fields and actions simply absent.

The test: can you describe who the app is for in one sentence, and would someone outside that role find it useless? "Approve purchase requisitions for cost center managers" passes. "Maintain business partner master data (all views)" doesn't — that's a database table with a UI, not a role-based app.

Role-based is the principle the others serve. If an app tries to serve every role, it serves none well — it becomes the monolithic transaction Fiori was created to replace.

2. Adaptive: one app, every device

Adaptive means the app reshapes itself for phones, tablets, and desktops — not three apps, not a "mobile version," but one app that responds to its container. The FlexibleColumnLayout collapsing from three columns to a single drill-down on phones is the textbook example. So is the DynamicPage header that snaps shut on scroll to preserve viewport space.

This goes deeper than layout. Adaptive covers input methods (touch targets sized for fingers, keyboard shortcuts for desktop power users), density (cozy mode on touch devices, compact where mouse precision allows), and information priority (the phone shows the three critical fields; the desktop shows all twelve).

The common failure: an app that's technically responsive — nothing overlaps at 375 pixels — but unusable, because the primary action is buried three scrolls down. Adaptive isn't "it renders." It's "the task is still completable."


3. Coherent: familiar everywhere

Coherent means a user who learned one Fiori app can operate the next one. Same navigation patterns, same filter bar behavior, same message handling, same terminology. The List Report in procurement works like the List Report in sales — because it's the same floorplan driven by the same rules.

This is where Fiori Elements earns its keep: floorplans enforce coherence structurally, not just by convention. But coherence extends beyond controls. It covers language (the i18n bundles use consistent terms — "Save" always means save, never "Store" or "Persist"), icons (the SAP icon font, used with consistent meaning), and flows (draft handling works the same in every transactional app).

For custom freestyle apps, coherence is a discipline, not a default. It means reusing the floorplan patterns even when hand-building, following the Fiori design guidelines, and resisting the urge to invent a novel navigation scheme because it seemed clever in a workshop.

Coherence compounds. Each coherent app makes the next one easier to learn; each incoherent one taxes every user who touches it. It's the principle with the highest organizational ROI and the least visible individual credit.

4. Simple: one task, minimal chrome

Simple is the famous "1-1-3" rule: one user, one use case, three screens maximum. A Fiori app does one thing. Not a module, not a process end-to-end — one task. "Approve leave requests." "Create a sales order." "Check stock levels."

Simplicity shows in what's removed: the twenty fields the role never touches, the three tabs for edge cases, the configuration options that belong in a different app. The launchpad model supports this — instead of one monster app, you ship five focused ones and let roles compose them.

It also shows in defaults. A simple app pre-fills what it can, picks the sensible default, and asks only for what's genuinely unknowable. Every field the user must fill is a small tax; simplicity minimizes the bill.

The tension: enterprise reality is complex, and "simple" can feel like "dumbed down" to experts who live in the system eight hours a day. The answer isn't to cram complexity back in — it's the right app for the right role. The expert gets their dense analytical app; the occasional user gets their three-screen flow.


5. Delightful: the details that earn trust

Delightful is the principle people skip, and it's the one users feel most. It covers the micro-interactions: the smooth transition when navigating, the toast confirming a save, the empty state that explains what to do instead of showing a blank screen, the loading skeleton that sets expectations instead of a spinner that says nothing.

Delight also means forgiveness. Draft handling that preserves work across sessions. Undo for destructive actions where feasible. Validation messages that say what's wrong and how to fix it, attached to the field, not dumped in a dialog. An app that forgives mistakes feels delightful; one that punishes them feels hostile, regardless of polish.

And it means performance as a feature. An app that responds instantly feels delightful; the same app with a two-second lag on every interaction feels broken. This is why the technical guidance — OData batching, $select to limit payloads, client-side caching — is ultimately a design concern, not just an engineering one.


How the principles work together

They're not a checklist to apply independently — they constrain each other. Role-based scoping enables simplicity. Coherence makes simplicity safe. Adaptive keeps the simple app simple on every device. Delightful is what the other four feel like when they're done well.

When evaluating or designing a Fiori app, run through all five: Who's the role? Does it adapt? Is it coherent with the apps around it? Is it simple — really one task? And do the details feel cared for? An app that passes all five is a Fiori app in more than name.


Bottom line

Role-based, adaptive, coherent, simple, delightful. Five words that decide whether an app feels like Fiori or just looks like it. The principles are easy to quote and hard to practice — especially simplicity, which demands saying no to features. But the apps users actually love are the ones where all five hold.

Read more →
sapui5tutors

Fiori Elements Floorplans: List Report and Object Page Explained

11:42:00

"The simplest app you can build on a single OData service" — if you've been around Fiori Elements, you've heard this line. It refers to the List Report floorplan: point the framework at an OData entity set, add a few annotations, and you get a working app with search, sorting, filtering, and navigation. No view code. No controller code. Just metadata.

This post explains the two floorplans that carry most Fiori apps — List Report and Object Page — how they work together, and what the framework actually generates from your annotations.


What a floorplan is

A floorplan is a standardized page layout with built-in behavior. Instead of designing each screen from scratch, you pick the floorplan that matches the user's task, and the framework renders it according to rules driven by OData annotations. Same floorplan, same behavior, across every app — that's where Fiori's consistency comes from.

Fiori Elements offers several floorplans (Overview Page, Analytical List Page, Worklist), but two dominate real projects: List Report for finding and acting on collections, and Object Page for viewing and editing a single object. They almost always appear as a pair.

A floorplan is a contract: you supply annotated OData, the framework supplies the entire UI. Your job shifts from building screens to describing data well.

List Report: find it, then act on it

The List Report answers "which one?" — the user searches, filters, sorts a collection, picks an item, and navigates to its Object Page. Purchase orders, sales orders, employee records, service tickets: if the task starts with a list, it starts here.

What the framework generates from annotations:

The filter bar comes from @UI.SelectionFields — each listed property becomes a filter field. Add @UI.FilterFacets and related entities get their own filter groups.

The table comes from @UI.LineItem — each entry becomes a column, in order, with the specified labels and formatting. This is why the annotation vocabulary matters so much: the annotation is the UI specification.

annotate service.Orders with @(
  UI.SelectionFields : [ status, customer_ID ],
  UI.LineItem : [
    { $Type : 'UI.DataField', Value : orderID, Label : 'Order' },
    { $Type : 'UI.DataField', Value : customer.name, Label : 'Customer' },
    { $Type : 'UI.DataField', Value : netAmount, Label : 'Net Value' },
    { $Type : 'UI.DataField', Value : status, Label : 'Status',
      Criticality : statusCriticality }
  ]
);

Four annotation entries, and the framework renders a filter bar with two filters plus a four-column table with status highlighting. The Criticality reference even colors the status column — green, yellow, red — with no UI code.

Actions come from bound OData actions or from @UI.DataFieldForAction entries. They render as toolbar buttons, enabled or disabled based on the action's applicability annotations.

This is the "simplest app" claim made concrete: one entity set, one annotation block, and you have a searchable, sortable, filterable list with navigation. The generator in Fiori tools scaffolds exactly this in about two minutes.


Object Page: everything about one thing

The Object Page answers "tell me everything about this one" — the user arrives from the List Report (or a tile, or a notification) and sees the full picture: header attributes, line items, related entities, attachments, all editable according to the draft and authorization annotations.

Its structure mirrors the DynamicPage control from freestyle UI5, because that's what the framework renders under the hood:

The header comes from @UI.HeaderInfo (title, description, image) plus @UI.FieldGroup annotations grouped into @UI.Facets. Each facet becomes a section — either a field group rendered as a form, or a reference facet pointing at a related entity set.

Line items — the order's items, the employee's assignments — come from @UI.LineItem on the related entity, surfaced through a UI.ReferenceFacet. The framework renders them as embedded tables with their own actions.

annotate service.Orders with @(
  UI.HeaderInfo : {
    TypeName : 'Order',
    Title : { Value : orderID },
    Description : { Value : customer.name }
  },
  UI.Facets : [
    { $Type : 'UI.ReferenceFacet', Label : 'General',
      Target : '@UI.FieldGroup#General' },
    { $Type : 'UI.ReferenceFacet', Label : 'Items',
      Target : 'items/@UI.LineItem' }
  ],
  UI.FieldGroup #General : {
    Data : [
      { Value : orderDate, Label : 'Order Date' },
      { Value : status, Label : 'Status' },
      { Value : netAmount, Label : 'Net Value' }
    ]
  }
);

Two facets: a General form section and an Items table section. Add a third facet pointing at an attachment entity and you get an attachments section. The page grows by annotation, not by code.


How they connect: navigation

The List Report → Object Page navigation needs no configuration when both floorplans target the same OData service. The framework wires it through the entity's navigation properties: tapping a row navigates to the Object Page bound to that entity instance, with the key in the URL hash for bookmarking.

Cross-entity navigation — from an order's Object Page to the customer's Object Page — works through @UI.DataFieldForIntentBasedNavigation or plain navigation properties. And intent-based navigation (semantic object + action) lets a List Report link out to apps the generator never knew about, resolved by the launchpad at runtime.

The List Report finds, the Object Page explains. If your app's flow is "search a collection, inspect one item, act on it," these two floorplans are the whole app — everything else is annotations.

When floorplans aren't enough

Floorplans cover standard patterns brilliantly and custom patterns not at all. The escape hatches, in increasing order of effort:

Building blocks let you embed floorplan pieces (a filter bar, a table, a form) inside a freestyle app. You get the annotation-driven behavior for the standard parts and hand-built UI for the exotic parts.

Custom sections and columns extend the List Report and Object Page themselves — a custom facet on the Object Page, a custom column in the List Report table, a custom action with its own dialog. Most real projects land here: 90% floorplan, 10% custom.

Freestyle is the full custom build. Reach for it when the UX doesn't map to any floorplan — complex wizards, canvas-style editors, highly custom dashboards. You lose the free behavior, so make sure the design genuinely needs it.


Bottom line

List Report for collections, Object Page for single objects, annotations as the UI specification. Learn the dozen annotations that drive these two floorplans and you can build — and more importantly, maintain — most Fiori apps without writing view code. The framework does the rendering; your expertise goes into the data model where it belongs.

Read more →
sapui5tutors

Responsive SAPUI5 Apps: From Phone to Desktop — FCL, DynamicPage and Grid

11:40:00

"It works on desktop" isn't enough anymore. The same Fiori app gets opened on a phone in a warehouse, a tablet in a meeting, and a widescreen monitor at a desk. SAPUI5 has real machinery for adapting to all three — but it doesn't happen by accident. The controls you choose determine how gracefully your app reshapes itself.

This post covers the three workhorses of responsive UI5 design: FlexibleColumnLayout, DynamicPage, and the Grid — plus the habits that make them work.


FlexibleColumnLayout: the master-detail standard

If your app has a master-detail flow (and most Fiori apps do), sap.f.FlexibleColumnLayout is the container to build it in. It manages up to three columns — Begin, Mid, End — and automatically collapses them based on screen width.

<f:FlexibleColumnLayout id="fcl"
    layout="TwoColumnsMidExpanded"
    stateChange="onStateChange">
  <f:beginColumnPages>
    <mvc:XMLView viewName="my.app.view.Master"/>
  </f:beginColumnPages>
  <f:midColumnPages>
    <mvc:XMLView viewName="my.app.view.Detail"/>
  </f:midColumnPages>
</f:FlexibleColumnLayout>

The magic is in the layout property and the framework's layout breakpoints. On a desktop, TwoColumnsMidExpanded shows master and detail side by side. On a phone, the same layout value renders as a single full-screen column with automatic back navigation — the framework handles the column-to-fullscreen translation, including the arrow button to go back.

You rarely set layout values by hand. The standard pattern uses the FlexibleColumnLayoutSemanticHelper, which computes the right next layout from the current one:

_onOrderSelect: function (oEvent) {
  var oFCL = this.byId("fcl");
  var oHelper = this._getFclHelper(oFCL);
  var oNextUIState = oHelper.getNextUIState(1); // 1 = show detail
  oFCL.setLayout(oNextUIState.layout);
  this.getOwnerComponent().getRouter().navTo("detail", {
    orderId: sOrderId,
    layout: oNextUIState.layout
  });
}

Storing the layout in the route (:layout: pattern parameter) keeps the back button and bookmarks working — deep-linking into a detail view restores the right column arrangement.

FCL's real value isn't the columns — it's that phone behavior comes free. Master-detail navigation that would need custom code in a SplitApp just works, including the back button, because the control owns the responsive logic.

DynamicPage: headers that collapse gracefully

The sap.f.DynamicPage solves a different problem: the object page with a big header (title, attributes, KPIs, tabs) that needs to stay usable while scrolling through long content. Its header has two states — expanded and snapped — and it pins the title when collapsed.

<f:DynamicPage id="detailPage"
               toggleHeaderOnTitleClick="true">
  <f:title>
    <f:DynamicPageTitle>
      <f:heading>
        <Title text="{OrderID}"/>
      </f:heading>
      <f:actions>
        <Button text="Edit" press="onEdit"/>
      </f:actions>
    </f:DynamicPageTitle>
  </f:title>
  <f:header>
    <f:DynamicPageHeader pinnable="true">
      <!-- Object attributes, KPI tiles, micro charts -->
      <f:content>
        <FlexBox wrap="Wrap">
          <ObjectAttribute title="Customer" text="{CustomerName}"/>
          <ObjectAttribute title="Status" text="{Status}"/>
          <ObjectNumber number="{NetAmount}" unit="{Currency}" title="Net Value"/>
        </FlexBox>
      </f:content>
    </f:DynamicPageHeader>
  </f:header>
  <f:content>
    <IconTabBar>
      <!-- line items, attachments, notes -->
    </IconTabBar>
  </f:content>
</f:DynamicPage>

On scroll, the header collapses to just the title row — actions stay reachable, content gets the space. On phones this matters enormously: a 400-pixel header on a 700-pixel viewport leaves no room for actual content. The pinned title means users never lose context about which object they're looking at.

The pinnable header lets users pin it open if they want the KPIs visible while scrolling — a small touch, but the kind that makes power users happy.


Grid: responsive form layouts without media queries

For everything that isn't master-detail or object-page — dashboards, forms, overview pages — the sap.ui.layout.Grid gives you a 12-column responsive grid with zero CSS:

<l:Grid defaultSpan="XL3 L3 M6 S12" class="sapUiSmallMarginTop">
  <l:content>
    <GenericTile header="Open Orders" subheader="This month" frameType="OneByOne">
      <TileContent>
        <NumericContent value="47" icon="sap-icon://sales-order"/>
      </TileContent>
    </GenericTile>
    <GenericTile header="Overdue" subheader="Needs attention" frameType="OneByOne">
      <TileContent>
        <NumericContent value="6" valueColor="Error" icon="sap-icon://alert"/>
      </TileContent>
    </GenericTile>
    <!-- more tiles... -->
  </l:content>
</l:Grid>

defaultSpan="XL3 L3 M6 S12" reads as: on extra-large and large screens each tile takes 3 of 12 columns (4 across), on medium 6 (2 across), on small 12 (full width, stacked). One attribute, four layouts. That's the whole responsive story for card-based pages.

For forms, pair the Grid with sap.ui.layout.form.SimpleForm, which has its own responsive layout="ResponsiveGridLayout" — labels above fields on phones, beside fields on desktop, handled automatically.

Reach for the Grid whenever you're tempted to write CSS media queries in a UI5 app. The framework's breakpoints (S/M/L/XL) are tested across the control library; hand-rolled breakpoints fight the controls instead of working with them.

Habits that make it all work

Test at 3 widths, not 30. Phone (~375px), tablet (~768px), desktop (~1440px). If it works at those three, the in-between sizes almost always behave. Browser dev tools device emulation is fine for layout checks.

Hide, don't squish. A table with 8 columns on a phone is unreadable whether you shrink or scroll. Use minScreenWidth on columns to drop the less important ones on small screens, or demandPopin to reformat them as stacked labels. Prioritize: which 3 columns would a phone user actually need?

Touch targets matter. Fiori's cozy/compact density helps — sap.m.List items and buttons are comfortably tappable in cozy mode. If your app targets phones, don't force compact density globally; let the device decide via the standard density helper in Component.js.

Navigation patterns differ. On desktop, master-detail shows both columns; the user selects and inspects. On phones, it's a drill-down: list → tap → detail → back. FCL handles this, but your content should respect it too — don't put critical actions only in the master column's toolbar where phone users might miss them.

Images and charts need explicit care. Layout controls adapt; content doesn't always. A vizFrame chart needs its own responsive handling, and large images should use densityAware or CSS max-width. Test charts on phones — legends and axis labels are the usual casualties.


Bottom line

Responsive UI5 isn't a separate mode you bolt on — it's a consequence of choosing the right containers. FCL for master-detail, DynamicPage for object pages, Grid for everything else. Pick those three well and the phone/tablet/desktop adaptations mostly take care of themselves; fight them with custom layouts and you'll be debugging breakpoints forever.

Read more →
sapui5tutors

MessageBox vs MessageToast in SAPUI5: User Feedback Done Right

11:39:00

Something went wrong. The user needs to know. Do you block the screen with a modal dialog, or flash a small toast at the bottom and let them keep working? SAPUI5 gives you both — MessageBox and MessageToast — and mixing them up is one of those small UX sins that makes an app feel unpolished.

Here's how to choose, with code for the patterns you'll actually use.


The fundamental difference

MessageBox is modal and blocking. It darkens the screen, demands attention, and waits for the user to click something before they can continue. It's a conversation: the app asks, the user answers.

MessageToast is non-modal and transient. A small strip slides up from the bottom, shows a message for a few seconds, then disappears on its own. The user never has to dismiss it and never stops working. It's an announcement, not a conversation.

Ask yourself: does the user need to decide something, or just know something? Decision → MessageBox. Awareness → MessageToast.

MessageBox: errors, warnings, confirmations

The MessageBox API is static — no instantiation, just calls. The three you'll use constantly:

sap.ui.require([
  "sap/m/MessageBox",
  "sap/m/MessageToast"
], function (MessageBox, MessageToast) {

  // Error: something failed, user must acknowledge
  MessageBox.error("The order could not be saved. Check the highlighted fields and try again.", {
    title: "Save failed",
    details: sTechnicalDetails,  // expandable "Details" link
    actions: [MessageBox.Action.CLOSE]
  });

  // Warning: proceed with caution
  MessageBox.warning("This customer has 3 overdue invoices. Create the order anyway?", {
    title: "Credit check",
    actions: [MessageBox.Action.YES, MessageBox.Action.NO],
    onClose: function (sAction) {
      if (sAction === MessageBox.Action.YES) {
        that._createOrderAnyway();
      }
    }
  });

  // Confirmation: destructive or significant action
  MessageBox.confirm("Delete 4 selected line items? This cannot be undone.", {
    title: "Confirm deletion",
    actions: [MessageBox.Action.DELETE, MessageBox.Action.CANCEL],
    emphasizedAction: MessageBox.Action.DELETE,
    onClose: function (sAction) {
      if (sAction === MessageBox.Action.DELETE) {
        that._deleteItems();
      }
    }
  });
});

A few details worth knowing. The details parameter on error() adds an expandable section — perfect for stashing the technical message or backend error payload while keeping the main text human-readable. emphasizedAction highlights the primary button, which matters for destructive confirmations where the safe choice should be visually obvious... or rather, where the intended choice should stand out.

MessageBox also has success() and information() variants, but use them sparingly. A modal dialog celebrating every successful save gets exhausting fast. Which brings us to...


MessageToast: lightweight confirmations

MessageToast is one line, no decisions, no drama:

MessageToast.show("Order 4711 saved");
// with a wider toast for longer text
MessageToast.show("Draft saved. It will be submitted automatically at 18:00.", {
  width: "24rem",
  duration: 5000  // milliseconds before it fades
});

The classic use cases: "Saved", "Copied to clipboard", "Draft discarded", "Filter applied — 12 results". The action completed, nothing is wrong, the user just deserves acknowledgment. A toast says "noted" without interrupting flow.

Toasts stack politely — firing several in quick succession queues them rather than overlapping. And they're automatically positioned above the mobile bottom nav if your app uses one, so they don't hide navigation.

The most common MessageToast mistake: using it for errors. A toast that says "Save failed" and vanishes after 3 seconds is a UX bug — the user may never see it, and even if they do, they can't act on it. Errors need the persistence of a MessageBox (or better, the MessageManager — see below).

Where MessageManager fits

There's a third player worth mentioning. When validation fails on form fields — a missing required value, a badly formatted date — neither a modal nor a toast is ideal. You want the error attached to the field itself, with a summary the user can click through.

That's sap.ui.core.message.MessageManager. Register it on the view, add messages against binding paths, and the framework routes them to the right controls automatically:

// in the controller
var oMessageManager = sap.ui.getCore().getMessageManager();
oView.setModel(oMessageManager.getMessageModel(), "message");
oMessageManager.registerObject(oView, true);

// when validation fails
oMessageManager.addMessages(
  new sap.ui.core.message.Message({
    message: "Delivery date cannot be in the past",
    type: sap.ui.core.MessageType.Error,
    target: "/Orders('4711')/DeliveryDate",
    processor: oODataModel
  })
);

Fields bound to that path show ValueState.Error with the message inline. Pair it with a MessageView in a popover for the "3 errors — click to navigate" summary pattern. For form validation, this beats both MessageBox and MessageToast.


Decision cheat sheet

MessageBox.error — operation failed and the user must know before continuing. Always include what happened and what to do next. Stash technical details in details.

MessageBox.warning / confirm — the user is about to do something with consequences: deleting, overwriting, proceeding despite a risk. The dialog exists to get an explicit decision.

MessageToast — action succeeded or a background event occurred; no action needed. Keep the text under ~10 words. If the message needs more than a glance, it's not a toast.

MessageManager — field-level validation in forms. Errors live on the controls, with an optional summary popover.

Nothing at all — underrated option. Not every state change needs announcing. If the UI already shows the result (the new item appears in the list, the status flips to "Saved"), an additional toast is noise. Experienced Fiori designers treat silence as a valid feedback choice and reserve toasts for moments when the UI alone doesn't confirm what happened.


Bottom line

Modal for decisions and failures, toast for confirmations, MessageManager for field validation, silence when the UI speaks for itself. Get this small choice right consistently and the whole app feels calmer and more professional — users can't always say why, but they notice.

Read more →
sapui5tutors

manifest.json: The SAPUI5 App Descriptor Explained with Examples

11:37:00

Every modern SAPUI5 app has a manifest.json sitting in its webapp folder, quietly running the show. It declares the app's identity, its models, its routing, its i18n files, even which UI5 libraries to load. And yet most developers only touch it when something breaks — a route that doesn't resolve, a model that's suddenly undefined.

Time to fix that. Here's what each section does, with an annotated example you can keep as a reference.


What manifest.json actually is

Think of manifest.json as the app's birth certificate plus instruction manual. When the Component loads, the framework reads this file first and configures everything from it: the app ID and version, the models to instantiate, the routing table, the resource bundle for translations.

Before manifest.json existed, all of this lived in JavaScript — Component.js metadata, manual model creation in init(), routing configured in code. It worked, but every app reinvented the same boilerplate. The descriptor moved that configuration into declarative JSON, which is easier to read, easier to validate, and tooling-friendly (the Fiori generators and editors understand it natively).

The mental model: Component.js is code, manifest.json is configuration. If it describes what the app needs rather than how it behaves, it belongs in the manifest.

The three top-level sections

A manifest has three main blocks. Here's the skeleton:

{
  "sap.app": {
    // WHO is this app: id, version, title, data sources
  },
  "sap.ui": {
    // HOW it presents: device types, supported themes
  },
  "sap.ui5": {
    // WHAT it needs: models, routing, resources, dependencies
  }
}

sap.app holds identity: the app id (which must match the Component namespace), type: "application", version numbers, and the human-readable title pulled from the i18n bundle. It also declares dataSources — named OData services the app consumes.

sap.ui is small but important: technology: "UI5", and deviceTypes declaring whether the app supports desktop, tablet, and phone. This feeds the Fiori launchpad's filtering — a phone-only app won't be offered on desktop.

sap.ui5 is where the real work happens: dependencies, models, routing, resource bundles. Most of your editing time goes here.


Annotated example: the sap.ui5 section

This is the part worth studying line by line:

"sap.ui5": {
  "dependencies": {
    "minUI5Version": "1.120.0",
    "libs": {
      "sap.m": {},
      "sap.ui.core": {},
      "sap.f": {}
    }
  },
  "models": {
    "i18n": {
      "type": "sap.ui.model.resource.ResourceModel",
      "settings": {
        "bundleName": "my.app.i18n.i18n",
        "supportedLocales": ["en", "de"],
        "fallbackLocale": "en"
      }
    },
    "": {
      "dataSource": "mainService",
      "settings": {
        "synchronizationMode": "None",
        "operationMode": "Server",
        "autoExpandSelect": true
      }
    }
  },
  "routing": {
    "config": {
      "routerClass": "sap.m.routing.Router",
      "viewType": "XML",
      "async": true,
      "viewPath": "my.app.view",
      "controlId": "app",
      "controlAggregation": "pages"
    },
    "routes": [
      {
        "name": "master",
        "pattern": "",
        "target": "master"
      },
      {
        "name": "detail",
        "pattern": "detail/{orderId}",
        "target": "detail"
      }
    ],
    "targets": {
      "master": {
        "viewName": "Master",
        "viewLevel": 1
      },
      "detail": {
        "viewName": "Detail",
        "viewLevel": 2
      }
    }
  },
  "resources": {
    "css": [
      { "uri": "css/style.css" }
    ]
  }
}

A few things to notice. The i18n model is a named model pointing at the resource bundle — that's why {i18n>title} bindings work everywhere. The unnamed "" model is the default OData model, wired to the mainService data source declared under sap.app. Models declared here are created automatically at startup — no setModel() calls in init() needed.

The routing config sets defaults for every route: which router class, that views are XML and loaded asynchronously, and — critically — controlId plus controlAggregation, which tell the router where to place navigated views. Get controlId wrong and navigation silently does nothing, which is one of the most common manifest debugging sessions.


Route patterns and parameters

The pattern is the hash fragment the route responds to. An empty pattern "" is the default route — what loads when the app opens. "detail/{orderId}" captures a segment into a parameter the detail controller reads:

onInit: function () {
  this.getOwnerComponent().getRouter()
    .getRoute("detail")
    .attachPatternMatched(this._onOrderMatched, this);
},

_onOrderMatched: function (oEvent) {
  var sOrderId = oEvent.getParameter("arguments").orderId;
  this.getView().bindElement("/Orders('" + sOrderId + "')");
}

Optional parameters use the :param: syntax — "detail/{orderId}/:tab:" matches with or without the tab segment. Query parameters (?filter=open) are available too, though they're less common in Fiori apps where state usually lives in the binding.

If navigation "does nothing" — no error, no view change — check three things in order: the route name matches the navTo call, the pattern matches the hash, and controlId points at a control that actually exists in the root view.

Data sources: naming your backends

Under sap.app, the dataSources block gives each backend a name:

"sap.app": {
  "id": "my.app",
  "type": "application",
  "dataSources": {
    "mainService": {
      "uri": "/sap/opu/odata/sap/ZORDER_SRV/",
      "type": "OData",
      "settings": {
        "odataVersion": "2.0"
      }
    }
  }
}

The model then references it by name ("dataSource": "mainService"). This indirection is what makes destinations and flexible deployment work — the URI here is relative, and the approuter or launchpad resolves it against the real backend at runtime. Hardcoding absolute backend URLs in the manifest is a classic mistake that breaks the moment the app moves between systems.


Common pitfalls

Trailing commas. JSON has no mercy — one trailing comma and the whole descriptor fails to parse, usually with an unhelpful error. Use an editor with JSON validation.

Namespace mismatch. The sap.app/id must match the Component's namespace exactly, including case. my.app vs my.App will cost you an afternoon.

Models in init(). If you declare models in the manifest and create them in Component.js init(), the code version wins and the manifest version is silently ignored. Pick one place — the manifest — and delete the code.

Forgetting async: true. Without it, views load synchronously, blocking the UI thread. There's no good reason to leave it off in a new app.


Bottom line

The manifest is the app's single source of truth for configuration: identity in sap.app, device support in sap.ui, and models, routing, and resources in sap.ui5. Learn to read it fluently and half of all "why isn't my app working" mysteries solve themselves in the first five minutes.

Read more →