From ui5-modernization
Auto-fixes UI5 XML view/fragment errors for no-globals, no-ambiguous-event-handler, and no-deprecated-api linter rules by adding core:require declarations, fixing event handler prefixes, and handling bind($control) references.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ui5-modernization:fix-xml-globalsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill fixes XML view and fragment issues that the UI5 linter detects but cannot auto-fix because they require understanding of module paths and handler locations.
references/placement-and-binding.mdscripts/README.mdscripts/__fixtures__/nested_function_shadow.jsscripts/__fixtures__/no_this.jsscripts/__fixtures__/not_found.jsscripts/__fixtures__/this_in_comment.jsscripts/__fixtures__/this_in_string.jsscripts/__fixtures__/uses_this_alias.jsscripts/__fixtures__/uses_this_arrow.jsscripts/__fixtures__/uses_this_bare.jsscripts/__fixtures__/uses_this_member.jsscripts/__fixtures__/uses_this_nested_in_if.jsscripts/__fixtures__/xml/has_bind.fragment.xmlscripts/__fixtures__/xml/multi_line.fragment.xmlscripts/__fixtures__/xml/needs_bind.fragment.xmlscripts/__fixtures__/xml/no_this_no_bind.fragment.xmlscripts/verify-this-bind.jsscripts/verify-this-bind.test.jsThis skill fixes XML view and fragment issues that the UI5 linter detects but cannot auto-fix because they require understanding of module paths and handler locations.
| Rule ID | Message Pattern | This Skill's Action |
|---|---|---|
no-globals | Access of global variable '...' (...) | Add core:require and use local name |
no-ambiguous-event-handler | Event handler '...' must be prefixed by a dot '.' or refer to a local name | Add . prefix for controller methods or add core:require for modules |
no-deprecated-api | Usage of space-separated list '...' in template:require | Convert to object notation |
Apply this skill when you see linter output like:
Main.view.xml:15:5 error Access of global variable 'formatter' (my.app.model.formatter) no-globals
Main.view.xml:20:5 warning Event handler 'onPress' must be prefixed by a dot '.' or refer to a local name no-ambiguous-event-handler
Main.view.xml:25:5 error Usage of space-separated list 'formatter helper' in template:require no-deprecated-api
no-globals - Fix Global Variable Access with core:requireWhen a formatter, type, or utility function is accessed via global namespace (e.g., my.app.formatter.formatDate), add a core:require declaration and use the local name.
Step 1: Add xmlns:core namespace if not present
<mvc:View
xmlns:mvc="sap.ui.core.mvc"
xmlns:core="sap.ui.core"
xmlns="sap.m">
Step 2: Add core:require on the nearest control that uses the module
Place core:require on the control that uses the global reference. If multiple controls use the same module, place it on their nearest common ancestor. See the "core:require Placement Rules" section below for details.
<!-- Single usage — core:require on the control itself -->
<Text core:require="{formatter: 'my/app/model/formatter'}"
text="{path: 'date', formatter: 'formatter.formatDate'}" />
<!-- Multiple usages — core:require on nearest common ancestor -->
<VBox core:require="{formatter: 'my/app/model/formatter'}">
<Text text="{path: 'date', formatter: 'formatter.formatDate'}" />
<Text text="{path: 'name', formatter: 'formatter.formatName'}" />
</VBox>
Step 3: Update references to use local name
<!-- Before -->
<Text text="{path: 'date', formatter: 'my.app.model.formatter.formatDate'}" />
<!-- After -->
<Text text="{path: 'date', formatter: 'formatter.formatDate'}" />
Step 4: Add .bind($control) for formatters that use this
If a formatter function accesses this internally, add .bind($control) to preserve the control context. This applies only to formatters — never add .bind($control) to event handlers or factory functions.
<!-- If formatter.formatWithContext uses 'this' to access the control -->
<Text core:require="{formatter: 'my/app/model/formatter'}"
text="{path: 'name', formatter: 'formatter.formatWithContext.bind($control)'}" />
no-ambiguous-event-handler - Fix Event HandlersEvent handlers must either:
.) to indicate controller methodController method (add dot prefix):
<!-- Before -->
<Button press="onPress" />
<!-- After -->
<Button press=".onPress" />
Module method (use core:require):
<!-- Before -->
<Button press="my.app.util.Handler.onPress" />
<!-- After - with core:require -->
<mvc:View
core:require="{
Handler: 'my/app/util/Handler'
}">
<Button press="Handler.onPress" />
</mvc:View>
no-deprecated-api - Fix Legacy template:require SyntaxConvert space-separated module list to object notation.
<!-- Before -->
<template:require name="formatter helper" />
<!-- After -->
<template:require name="{
formatter: 'my/app/model/formatter',
helper: 'my/app/util/helper'
}" />
When multiple globals are used, combine them in a single core:require on their nearest common ancestor.
<mvc:View
xmlns:mvc="sap.ui.core.mvc"
xmlns:core="sap.ui.core"
xmlns="sap.m"
core:require="{
formatter: 'my/app/model/formatter',
types: 'my/app/model/types',
utils: 'my/app/util/utils'
}">
no-globals - Fix Type Property in BindingsWhen a global is used in the type property of a binding object, it also needs core:require.
<!-- Before -->
<Input value="{
path: '/amount',
type: 'sap.ui.model.type.Currency'
}" />
<!-- After -->
<Input core:require="{Currency: 'sap/ui/model/type/Currency'}"
value="{
path: '/amount',
type: 'Currency'
}" />
This applies to all UI5 types (Currency, Date, Float, Integer, String, etc.) and custom type classes.
no-globals - Fix Factory FunctionsFactory functions (e.g., factory attribute on List) follow the same core:require pattern but never use .bind($control).
<!-- Before -->
<List items="{/items}" factory="my.app.util.ListFactory.createItem" />
<!-- After — no .bind($control) for factories -->
<List core:require="{ListFactory: 'my/app/util/ListFactory'}"
items="{/items}" factory="ListFactory.createItem" />
A module declared via core:require is only accessible to that element and its descendants — not siblings.
core:require directly on that controlcore:require on their nearest common ancestorPrefer granular placement over always using the root — it reduces unnecessary scope and makes the dependency clear at the point of use.
For detailed examples (granular placement across nested elements, scope errors with sibling elements), see references/placement-and-binding.md.
.bind($control)When a function is called via core:require, it does not automatically receive a this context. Whether to add .bind($control) depends on the usage type — formatter, factory, or event handler:
| Usage Type | .bind($control) | Why |
|---|---|---|
Formatter (inside {path:..., formatter:'...'} or {parts:[...], formatter:'...'}) | Yes, if function uses this | this = the control instance owning the binding. $control resolves to the ManagedObject at runtime. |
Factory (factory= attribute on aggregation binding) | No — never add .bind() | Factory functions receive (sId, oContext) as parameters. They don't use this for control context. Adding .bind() is unnecessary and incorrect. |
Event handler (press=, change=, confirm=, select=, valueHelpRequest=, etc.) | No — never add .bind() | Event handler resolution in XML with core:require calls the function directly with the event as parameter. Adding .bind() interferes with this resolution. |
How to detect usage type in XML:
{path:..., formatter:'Module.fn'} or {parts:[...], formatter:'Module.fn'}factory='Module.fn' attribute on an aggregation bindingpress=, change=, confirm=, cancel=, select=, selectionChange=, valueHelpRequest=, tokenUpdate=, close=, delete=, titlePress=The formatter-specific rule:
this, add .bind($control)$control (NOT $controller) — this binds to the control instancethis, omit .bind($control)<!-- Formatter that uses 'this' → must bind -->
<Text core:require="{formatter: 'my/app/model/formatter'}"
text="{path: 'name', formatter: 'formatter.formatWithContext.bind($control)'}" />
<!-- Formatter that does NOT use 'this' → no bind needed -->
<Text core:require="{formatter: 'my/app/model/formatter'}"
text="{path: 'date', formatter: 'formatter.formatDate'}" />
<!-- Factory → NEVER bind -->
<List core:require="{ListFactory: 'my/app/util/ListFactory'}"
items="{/items}" factory="ListFactory.createItem" />
<!-- Event handler → NEVER bind -->
<Button core:require="{Handler: 'my/app/util/Handler'}"
press="Handler.onExport" />
Detection: run scripts/verify-this-bind.js audit-fn. The script handles every this form in one pass — member access, dynamic property, bare arg (jQuery.proxy(fn, this), .call(this), .apply(this), .bind(this)), aliased (var self = this; self.foo), and arrow-inherited. Comments and string literals are stripped; nested non-arrow function bodies are excluded (they own their own this); arrow bodies are kept (they inherit). Standard UI5 types (sap.ui.model.type.*) never need binding and the script confirms this. Manual grep is not allowed for batch decisions — the script is canonical.
node scripts/verify-this-bind.js audit-fn --file <module.js> --fn <name> [--fn <name> ...]
Functions reported USES_THIS MUST have .bind($control) appended in XML. Functions reported NO_THIS MUST NOT have it. Do NOT classify by name shape — formatX / decideX / isX are not proof of pure-value formatters; only the source body is.
Why a script and not grep: searching for this\. (the dot form) silently misses bare-this idioms. jQuery.proxy(fn, this) passes this as a positional argument with no dot anywhere — an eyeball or a naive grep walks past it, the formatter looks pure, and .bind($control) is omitted. At runtime the proxied callback ends up with the wrong context and this.getModel(...) throws. The script's \bthis\b scan combined with alias tracking catches every variant in one go. Fall back to grep -nE '\bthis\b' <module.js> only if the script is unavailable.
For full before/after examples (formatter, handler, factory) see references/placement-and-binding.md.
Fragments use <core:FragmentDefinition> instead of <mvc:View>. The same placement principle applies, with one key difference:
Place core:require on the child control, NOT on FragmentDefinition. FragmentDefinition is a structural wrapper, not a real control. Place core:require on the actual root control inside it (e.g., Dialog, VBox) or on the nearest common ancestor of the controls using the module.
<!-- PREFERRED: core:require on Dialog (the actual root control) -->
<core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core">
<Dialog title="Settings"
core:require="{formatter: 'my/app/model/formatter', Actions: 'my/app/util/Actions'}">
<content>
<Input value="{path: 'name', formatter: 'formatter.toUpperCase'}" />
<Button text="Save" press="Actions.onSave" />
</content>
</Dialog>
</core:FragmentDefinition>
Exception: When a fragment has multiple direct children that all need the same module, FragmentDefinition becomes the nearest common ancestor — placing core:require there is acceptable.
For examples of multi-child fragments and scoped modules within fragments, see references/placement-and-binding.md.
When multiple modules share the same class name, use descriptive aliases:
<mvc:View xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m" xmlns:core="sap.ui.core"
core:require="{
ReportFormatter: 'my/app/report/Formatter',
KPIFormatter: 'my/app/kpi/Formatter'
}">
<Input value="{path: 'report', formatter: 'ReportFormatter.format'}" />
<Text text="{path: 'kpi', formatter: 'KPIFormatter.format'}" />
</mvc:View>
When a binding expression uses a formatter with multiple parts, the core:require is the same — only the global namespace in the formatter reference changes.
<!-- Before -->
<Text text="{
parts: [
{path: 'firstName'},
{path: 'lastName'}
],
formatter: 'my.app.model.formatter.formatFullName'
}" />
<!-- After (with core:require for formatter on the view root) -->
<Text text="{
parts: [
{path: 'firstName'},
{path: 'lastName'}
],
formatter: 'formatter.formatFullName'
}" />
Expression bindings that reference globals via the ${...} syntax also need core:require.
<!-- Before -->
<Text visible="{= ${/count} > 0}" text="{= my.app.model.formatter.formatCount(${/count})}" />
<!-- After -->
<Text visible="{= ${/count} > 0}" text="{= formatter.formatCount(${/count})}" />
The UI5 linter reports app-namespace globals in XML under the no-globals rule — the same rule used for sap.* and jQuery.* globals. This skill handles ALL no-globals in XML files, including:
sap.m.ButtonType.Accept, sap.ui.model.type.Currency → core:require the modulejQuery.sap.getModulePath → core:requirecom.example.app.utils.Handler.onPress → core:require the utility modulecom.example.app.model.formatter.formatDate → core:require the formatter moduleThe fix pattern is identical for all: add core:require attribute with the module path (dots → slashes), replace the dotted global with the local alias.
Before:
<core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core">
<Button press="com.example.app.utils.Handler.onPress" />
</core:FragmentDefinition>
After:
<core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core">
<Button core:require="{Handler: 'com/example/app/utils/Handler'}"
press="Handler.onPress" />
</core:FragmentDefinition>
Apply .bind($control) rules from the "When to Use .bind($control)" section — formatters: yes (if function uses this); factories: never; event handlers: never.
Important: Do NOT defer app-namespace XML globals to fix-linter-blind-spots (Phase 3, Step 3.2). That skill handles only JS app-namespace globals which the linter cannot detect. XML app-namespace globals ARE reported by the linter and MUST be fixed here in Phase 3.
Read the XML view/fragment file
Parse to identify linter errors by rule ID:
no-globals: Global namespace references in bindings (formatters, types, event handlers, factories)no-ambiguous-event-handler: Event handlers without proper prefixno-deprecated-api: Legacy template:require syntaxFor no-globals errors:
xmlns:core="sap.ui.core" to the root element if not presentmy.app.formatter → my/app/formatter)core:require on the actual root control (e.g., Dialog), not on FragmentDefinitionnode scripts/verify-this-bind.js audit-fn --file <module.js> --fn <name> [--fn <name> ...] (batch as many --fn flags as the file has formatters). Functions reported USES_THIS MUST have .bind($control) appended in the XML formatter ref. Functions reported NO_THIS MUST NOT. Factories and event handlers never bind regardless of their this usage.core:require object with all needed modules (use aliases for name conflicts)For no-ambiguous-event-handler errors:
line:col to locate the exact attribute. Read only that line.. for a bare handler name; or apply core:require + alias replacement for a namespace path.id, selectedKey, key, name, plain text, …) and MUST NOT be modified. One linter finding → one edit.For no-deprecated-api (template:require):
Write the updated file
Post-edit verification (mandatory). After all XML edits in this phase and before declaring done:
node scripts/verify-this-bind.js verify-xml \
--xml-root webapp \
--js-roots webapp/utils,webapp/model
Exit 0 → done. Exit 1 → fix the listed MISSING_BIND violations and re-run. The script is the gate; agent-side eyeballing is not.
Example 1: Basic — formatter + event handler
Given linter output:
Main.view.xml:15:5 error Access of global variable 'formatter' (my.app.model.formatter) no-globals
Main.view.xml:20:5 warning Event handler 'onPress' must be prefixed by a dot '.' or refer to a local name no-ambiguous-event-handler
Main.view.xml transformation:
<!-- Before -->
<mvc:View
controllerName="my.app.controller.Main"
xmlns:mvc="sap.ui.core.mvc"
xmlns="sap.m">
<Page title="Main">
<content>
<Text text="{path: 'date', formatter: 'my.app.model.formatter.formatDate'}" />
<Button text="Submit" press="onPress" />
</content>
</Page>
</mvc:View>
<!-- After -->
<mvc:View
controllerName="my.app.controller.Main"
xmlns:mvc="sap.ui.core.mvc"
xmlns:core="sap.ui.core"
xmlns="sap.m"
core:require="{
formatter: 'my/app/model/formatter'
}">
<Page title="Main">
<content>
<Text text="{path: 'date', formatter: 'formatter.formatDate'}" />
<Button text="Submit" press=".onPress" />
</content>
</Page>
</mvc:View>
Example 2: Advanced — granular placement, type binding, .bind($control)
Given linter output:
OrderDetail.view.xml:8:9 error Access of global variable 'formatter' (my.app.model.formatter) no-globals
OrderDetail.view.xml:10:13 error Access of global variable 'Currency' (sap.ui.model.type.Currency) no-globals
OrderDetail.view.xml:14:13 error Access of global variable 'Handler' (my.app.util.Handler) no-globals
OrderDetail.view.xml:18:9 warning Event handler 'onBack' must be prefixed by a dot '.' no-ambiguous-event-handler
<!-- Before -->
<mvc:View controllerName="my.app.controller.OrderDetail"
xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m">
<Page title="Order">
<content>
<Text text="{path: 'status', formatter: 'my.app.model.formatter.formatStatus'}" />
<VBox>
<Input value="{path: '/amount', type: 'sap.ui.model.type.Currency'}" />
<Text text="{path: 'note', formatter: 'my.app.model.formatter.formatNote'}" />
</VBox>
</content>
<footer>
<Bar>
<contentRight>
<Button text="Export" press="my.app.util.Handler.onExport" />
</contentRight>
</Bar>
</footer>
<headerContent>
<Button press="onBack" icon="sap-icon://nav-back" />
</headerContent>
</Page>
</mvc:View>
<!-- After -->
<mvc:View controllerName="my.app.controller.OrderDetail"
xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m" xmlns:core="sap.ui.core">
<Page title="Order"
core:require="{formatter: 'my/app/model/formatter'}">
<content>
<Text text="{path: 'status', formatter: 'formatter.formatStatus'}" />
<VBox>
<Input core:require="{Currency: 'sap/ui/model/type/Currency'}"
value="{path: '/amount', type: 'Currency'}" />
<Text text="{path: 'note', formatter: 'formatter.formatNote'}" />
</VBox>
</content>
<footer>
<Bar>
<contentRight>
<Button core:require="{Handler: 'my/app/util/Handler'}"
text="Export" press="Handler.onExport" />
</contentRight>
</Bar>
</footer>
<headerContent>
<Button press=".onBack" icon="sap-icon://nav-back" />
</headerContent>
</Page>
</mvc:View>
Placement decisions:
formatter used in two <Text> controls across content — core:require on Page (nearest common ancestor)Currency only used by one <Input> — core:require directly on InputHandler only used by one <Button> in footer — core:require directly on ButtonHandler.onExport is an event handler — no .bind($control) (event handlers never need it)onBack is a simple controller method — dot prefix addedPlace core:require on the nearest control that uses the module, or the nearest common ancestor when multiple controls share it — prefer granular placement over always using the root
Module paths in core:require use forward slashes (/), not dots
Multiple modules are separated by commas in the JSON object notation
Use descriptive aliases to avoid name conflicts between modules with the same class name
Always check the usage type first: formatters may need .bind($control) if the function uses this; factories and event handlers never need it
For fragments, place core:require on the actual child control (e.g., Dialog), not on FragmentDefinition unless it's the only common ancestor
Ensure the formatter/utility module exists at the specified path
For complex cases with dynamic module loading, consider using sap.ui.require in the controller instead
Name-pattern shortcuts are forbidden for this detection. Formatter names like formatX, decideX, isX, getX are not proof of pure-value behaviour. Common patterns that look pure but read this:
this.getModel(...), this.getBindingContext(), this.getId() — often inside helper branchesthis to a callback wrapper such as jQuery.proxy(fn, this), fn.call(this), fn.apply(this), or fn.bind(this) — the call has no . after this, so a casual read sees no member accessthis first (var self = this; ... self.getX()) — the body of the formatter never mentions this after the alias lineEach of these breaks at runtime when the formatter is called without .bind($control), because this is not bound to the control by default. The verify-this-bind.js script catches all of these forms in one pass; eyeballing or grep 'this\\.' misses them. Always run the script — never skip based on name.
Linter coordinates are authoritative for no-ambiguous-event-handler and no-globals identifier-rename. Never use grep / file-wide regex to "find all uses" of a flagged identifier. The linter has already done that lookup — every offending site is in its output. Editing lines NOT in the linter output is out of scope and almost certainly incorrect: a name that appears as an event handler can also appear as a control id, a selectedKey, a binding path, or a string literal — those uses are not handlers and rewriting them silently changes runtime semantics (e.g., breaking Fragment.byId lookups). Exception: core:require insertion is file-level reasoning (place once on nearest common ancestor); applies once per file, not per finding.
no-deprecated-api with "native HTML" or "SVG" messages), use fix-xml-native-htmlno-globals in JavaScript files (controller/utility global access), use fix-js-globals — it handles the JS-side equivalent of this skill's XML fixesclaude plugin install ui5-modernization@claude-plugins-officialFixes native HTML and SVG usage in UI5 XML views/fragments detected by the UI5 linter, replacing them with equivalent UI5 controls and icons.
Enforces SAP UI5 coding standards including async loading, ComponentSupport, CSP compliance, OData binding, i18n, TypeScript events, CAP integration, and form layouts (Form with ColumnLayout).
Reviews SAP Fiori/UI5 apps for design guidelines, MVC structure, OData patterns, performance, accessibility, and launchpad integration from source code.