A named model binding {ui>/rowMode}, {img>/x}, any name>/… | Drop the prefix, bind the field on the one default model: client->_bind( rowmode ) → {/ROWMODE}. structural-diff matches on the last path segment. A sap.uxap:ModelMapping (external→internal model remap, e.g. {Contact>firstName} where Contact maps jsonModel>/Employee) folds the same way — resolve the indirection statically and bind the leaf on the root; dropping the ModelMapping/ModelMappingBlock config control is lossy → IMPROVISED even though it renders identically (see the deviation-type note in the port-a-sample guide, "Generation notes") | CAPABILITIES "Named JSON models"; apps 162/163/164, 230 |
An i18n resource-model binding {i18n>KEY} (sap.ui.model.resource.ResourceModel) | No i18n model exists by design, not as a gap (user decision): a frontend translation store contradicts the thin-frontend principle — translation is a backend concern, ABAP translates texts natively (text elements / OTR, sy-langu) and serves the finished string as a bound model field. So: for a 1:1 demo-kit port, fold to the literal text (usually English from i18n.properties) and declare IMPROVISED (the sample's runtime language switch is lost); a real translatable app binds ABAP-translated texts instead — never propose frontend i18n support as a pr/ | recipe; app 233; CAPABILITIES "i18n texts" |
A typed / complex binding {path:'Q', type:'sap.ui.model.type.Integer'} | Raw binding-info string, braces escaped: v = |\{ path: 'Q', type: 'sap.ui.model.type.Integer' \}| — passes through to XMLView.create unmangled. The path: uses the upper-cased ABAP field name ('Q'→'PRICE'), not the original camelCase — no gate catches a stale path | CAPABILITIES "composite … types"; apps 164/129/033/171 |
A nested single object binding {transactionAmount/size} (control property → a sub-object, not an array) | Nested ABAP structure component in the row type; bind the relative sub-path {TRANSACTION_AMOUNT/SIZE}. Keep the nesting — do not flatten to {OBJ_FIELD} | CAPABILITIES "Nested single object"; app 171 |
A literal quote inside a binding-info pattern (pattern:'yyyy-MM-dd'T'HH:mm:ss' — the 'T') | Escape it backslash-quote \', written \\' in the ABAP |…| template (UI5's JSTokenizer rejects a doubled '') — matches how the original view.xml escapes it | app 183 (TypeDateTime) |
A sorter / sorter group:true on an aggregation | Keep the raw string; get the bare path via client->_bind( val = t path = abap_true ): |\{ path: '{ … }', sorter: \{ path: 'COL', group: true \} \}| | CAPABILITIES "Binding sorter"; app 039 |
A date-object property (CalendarAppointment.startDate, PlanningCalendar.startDate, DatePicker.dateValue) | Formatter at point of use: |\{ path: 'START_AT', formatter: 'Formatter.DateCreateObject' \}| + core:require="\{Formatter: 'z2ui5/model/formatter'\}" (POST_171). A plain string binding crashes | CAPABILITIES "Date-object"; apps 108/109 |
A standard date/time binding type (type:'sap.ui.model.type.Date'/DateTime/Time, or a core:require alias for it) | Keep the original binding-info 1:1 and add formatOptions.source matching the ABAP field: |\{ path: 'DATE', type: 'DateType', formatOptions: \{ style: 'short', source: \{ pattern: 'yyyy-MM-dd' \} \} \}|. Without source the type reads a JS Date object out of the model, which a JSON model can never carry — every format() raises a FormatException and the field stays empty. The render gate cannot see it (it mocks the model); the linter rule date-type-without-source does | CAPABILITIES "Date-object"; apps 181/182/183, 282 |
| A boolean attribute fed from an ABAP variable | a( n = \editable` b = flag )— passbinstead ofv (a literal is just `` v = \true` ``); never feed abap_true/abap_false raw into v | recipe "Booleans"; app 007 |
| A property computed from several bound values | UI5 expression binding, _bind inlined: |\{= ${ client->_bind( a ) } && ${ client->_bind( b ) } \}| — no event round-trip. Which literal form depends on where the paths come from: a _bind( ) result must be interpolated, so the expression goes in a |…| template with the outer braces escaped \{ \} (the row's form). An expression over a relative row field inside a bound aggregation has nothing to interpolate — write it as a plain backtick literal, `{= ${END} ? Formatter.DateCreateObject(${END}) : null }`, so every brace reaches the attribute verbatim. Escaping row-field braces inside a template collapses them and the attribute silently becomes garbage that no gate reads as a binding | recipe; apps 007, 053; the relative-field form: app 220 |
The controller reads an event/source value (evt.getSource().getId()) | Transport it, don't fake it: t_arg value $event.oSource.sId / ${COL}, read back with get_event_arg( ). A bare {COL} is not resolved here | recipe "Data binding & events"; app 005 |
MessageToast.show("…" + evt.x) (text built on the client) | Client-composed template, roundtrip-free. Exact t_arg tuple order = object, method, template, arg(s): client->follow_up_action( val = client->cs_event-control_global t_arg = VALUE #( ( \`MESSAGE_TOAST\` ) ( \`show\` ) ( \`Item selected: {0}\` ) ( \`${$parameters>/item}.getText()\` ) ) ). The wire token is MESSAGE_TOAST (not MessageToast); {0} is filled by the resolved arg. A template may START with {0} — get_t_arg quotes a leading {N}/{N?…} placeholder as a plain string (source: ^\{[0-9]+[?}] match), so a value-first toast needs no round-trip fallback. Button text is ${$source>/text}; menu-item text is ${$parameters>/item}.getText(). An arg is a full UI5 expression, not just a path — EventHandlerResolver parses the whole handler with BindingParser.parseExpression, so method calls, isA('…'), string concat and ternaries all work in an arg. But the grammar has no loop: a parent-chain walk must be unrolled to a fixed hop count, which fails when the control tree reshapes at runtime — sap.m.Menu re-parents items through a sap.m.MenuWrapper, so a nested item's parent MenuItem is two hops up while the submenu is closed and four once its popover exists. That is why the menu breadcrumb stays non-transportable (documented boundary, CAPABILITIES) and apps 060/061 keep the leaf ${$parameters>/item}.getText(). Measure the chain before betting an arg on getParent(). When the original pulls the module via core:require on the fragment/view root (core:require="\{MessageToast: 'sap/m/MessageToast'\}" for an inline press="MessageToast.show(…)") and you rewire to follow_up_action, that core:require is dropped — name it in the deviation: structural-diff treats core:require as an ordinary (non-xmlns) attribute and flags it missing | CAPABILITIES; apps 005/060/172 |
Custom CSS / raw markup (style.css, <h2>, a core:HTML content div) | core:HTML leaf, markup in the content attribute (no CDATA node exists). Two traps: (a) write the decoded literal markup — the original XML carries it entity-encoded (<div>), but you write <div>; the builder re-escapes on stringify, so copying the entities double-escapes them. (b) escape literal braces \{ \} in a backtick literal `…\{…\}…` — backtick passes \{ through to the serialized attribute; a |…| template would collapse \{→{ and re-crash (the reverse of the typed-binding row, which wants real braces and so uses the pipe) | CAPABILITIES "Custom CSS"; apps 026/028, 169 |
Controller .filter()/.sort() on oList.getBinding('items') | cs_event-binding_call (whitelisted methods/operators, compound filter groups supported) — the model stays untouched | CAPABILITIES "Controller-applied binding filter"; app 022 |
An imperative control method (open/close/toggleStyleClass/toDetail/expandToLevel/setHiddenInPopin…) | follow_up_action( val = cs_event-control_by_id t_arg = id/method/args ). A method listed in CONTROL_METHODS carries explicit arg kinds (extra args silently dropped — verify the kinds); a method not listed still runs when it is a public control method not matching the deny regex (destroy/bind/attach/setModel/… — the framework-invariant guard), so ordinary setters/toggles need no whitelist entry (source-verified in FrontendAction.js). A denylisted or argument-dropping need is a declared deviation + pr/. The id is the part nothing used to validate — a wrong one resolves to no control and the wire is silent; the linter rule frontend-action-unknown-id now checks every literal id against the ids the class's views declare (all slots) | CAPABILITIES "Frontend-action catalog"; recipe |
An anchored-open popup (byId(x).openBy(btn) — sap.m.Menu, TimePicker, DatePicker, sap.ui.unified.Menu) | control_by_id + openBy/toggleBy + $event.oSource.sId; declare the popup control in dependents. The openBy dispatch falls back to open(false, anchor, 'begin top', 'begin bottom', anchor) for a control without an own openBy (sap.ui.unified.Menu) — the same openBy wire works for every menu family. Only the keyboard flag and explicit dock constants stay dropped (NOTE) | CAPABILITIES "Frontend-action catalog"; apps 016/060, 227/228 (unified.Menu via the fallback) |
A control-created popup (new Dialog().open(), new PDFViewer().open()) | Build a core:FragmentDefinition, show with client->popup_display( val = … ) / popover_display( xml = … by_id = … ) (popover XML param is xml, not val) — or declare in mvc:dependents + control_by_id/open/openBy. A single-root fragment (<SomeControl …> with the namespaces on the control, no wrapper) has no FragmentDefinition — open the control directly, and structural-diff counts that root control (only mvc:View/core:FragmentDefinition are exempt) | CAPABILITIES "Popups & messages"; apps 019/044/229 |
A popup/popover bindElement | Fixed index bindElement('/Coll/0') (single record) → seed those fields at the default-model root and bind them absolutely (client->_bind( field )); no bind_element follow-up needed. Do not keep the fragment's relative {FIELD} form — with the element binding gone there is no context, JSONModel._getObject returns undefined and the control renders empty (seven ports shipped that; linter rule relative-binding-without-context). Per-row selection (index arrives from $event.oSource.getBindingContext()) → follow_up_action( val = cs_event-bind_element view = cs_view-popover t_arg = ( idx )( client->_bind( tab ) ) ). Pick by whether the bound record is fixed or row-driven | CAPABILITIES "Popups & messages"; app 229 (fixed), app 094 (per-row) |
Inlining a core:Fragment (the sample splits its view into *.fragment.xml referenced by <core:Fragment fragmentName="…"/>) | Inline the fragment content directly into the one port view. structural-diff unions every *.fragment.xml with the view and counts the core:Fragment reference elements as controls (only mvc:View/core:FragmentDefinition roots are exempt) — so the dropped core:Fragment references must be named in a deviation (same as the core:require rule). If the fragments declare different default xmlns (e.g. sap.uxap in one, sap.m in another), the single port view can only carry one default namespace, so some controls get a prefix the original lacked (uxap:ObjectPageSection vs ObjectPageSection) — an unavoidable namespace-representation NOTE. Asymmetry to remember: attribute checks are prefix-blind (simpleName), but control counts are prefix-sensitive. When you inline a BlockBase block or a nested mvc:View content-only, the block's own root mvc:View attributes (width="100%", height, …) are dropped too and surface as attr missing View.width — name the root-View attributes in the deviation as well, not just the block/html:div wrapper | CAPABILITIES "Popups & messages"; apps 233/234, 239 (BlockBase root-View attrs) |
A bindable property the controller sets imperatively (fcl.setLayout(x), oCtrl.setVisible(b)) | Prefer the bindable property over a frontend action: two-way bind it (layout="{/LAYOUT}") and update the model server-side — no CONTROL_METHODS entry needed. Only reach for follow_up_action/control_by_id when there is no bindable property for the effect (port-a-sample porting gotchas: "prefer a bindable property"). Gated: the linter rule settable-property-via-action reports a set…( ) whose name matches a bindable property of the addressed control — an association (selectedSection) and a function-typed property (asyncURLHandler) are excluded, because neither can be bound at all | CAPABILITIES "Frontend-action catalog"; app 234 (FlexibleColumnLayout.layout) |
The controller drives control state the port cannot bind at all — an association (setSelectedSection), a function-typed property (asyncURLHandler), a setter behind no property (setBadgeMinValue/setBadgeMaxValue: sap.m.Button declares exactly one badge property, badgeStyle, and keeps the bounds in private _badgeMinValue/_badgeMaxValue that Button.init resets to 1/9999), or a binding_call filter/sorter, which acts on the live aggregation binding and not on the model | Issue it from a named helper method, and call that helper AGAIN at the end of view_display( ), guarded on the ABAP field that describes it. view_display( ) destroys the slot and XMLView.create builds a fresh control tree, so everything set through a control call or a binding call is gone — while the field DESCRIBING it survives, and the app then claims a state it does not show. Re-issue the last-issued payload (022/235/557 park it in a PROTECTED filter_live — only PUBLIC attributes are serialized into the view model, and this is bookkeeping) rather than re-deriving it: that makes the rebuilt binding identical to the live one by construction. Guard it — a re-issue on the initial render is a no-op but not free (an extra action and a binding refresh on every start) — and skip a value equal to the control's own default, which a setter may reject and log as invalid (app 249's 1/9999). No navigation is needed to reach the second view_display( ): the framework's own bookmark restore ?app_start=<class>#/z2ui5-xapp-state=<draft> — the URL cs_event-clipboard_app_state hands out — carries no frontend id, so the handler takes factory_first_start → db_load(draft) and check_on_navigated( ) is true while check_on_init( ) stays false. An e2e leg for this must assert both halves (the surviving field AND the re-applied control state): asserting only the reset half passes happily on a port that never set anything, which is what kept the class invisible | canonical form: app 000's view_display( ) (the restored search filter, re-issued via follow_up_action( cs_event-binding_call … )); linter rule control-state-lost-on-rebuild; the worked list and what each one proves: , "live control state that must survive a view rebuild"; recipe gotchas "Client-side-only state does not survive a view rebuild" |