Loading...
Loading...
A skill for converting UI5 (SAPUI5/OpenUI5) projects to TypeScript.
npx skill4agent add cline/skills ui5-typescript-conversionThis document outlines how a UI5 (SAPUI5/OpenUI5) project can be converted to TypeScript. It consists of the following parts:
- Important general rules
- How the setup of the project needs to be changed
- Converting the code itself
- Converting tests (reference to separate file)
/**
* My cool controller, it does things.
*/
return Controller.extend("com.myorg.myapp.controller.BaseController", {
/**
* Convenience method for accessing the component of the controller's view.
* @returns {sap.ui.core.Component} The component of the controller's view
*/
getOwnerComponent: function () {
// comment
return Controller.prototype.getOwnerComponent.call(this);
},
...
});export default class BaseController extends Controller {
public getOwnerComponent(): UIComponent {
return super.getOwnerComponent() as UIComponent;
}
}/**
* My cool controller, it does things.
* @namespace com.myorg.myapp.controller
*/
export default class BaseController extends Controller {
/**
* Convenience method for accessing the component of the controller's view.
* @returns {sap.ui.core.Component} The component of the controller's view
*/
public getOwnerComponent(): UIComponent {
// comment
return super.getOwnerComponent() as UIComponent;
}
}"allowJs": truetsconfig.jsoncompilerOptionsanyany(this.getOwnerComponent() as any).getContentDensityClass();(this.getOwnerComponent() as AppComponent).getContentDensityClass()unknownsap/ui/core/Controlthis.byId(...)Route$PatternMatchedEvent(this.byId("form") as unknown as {setVisible: (v: boolean) => void}).setVisible(false);import SimpleForm from "sap/ui/layout/form/SimpleForm";
(this.byId("form") as SimpleForm).setVisible(false);src/types/@sapui5/types@openui5/typesget_project_infotypescript-eslint"ts-typecheck": "tsc --noEmit"package.json{
"compilerOptions": {
"target": "es2023",
"module": "es2022",
"moduleResolution": "node",
"skipLibCheck": true,
"allowJs": true,
"strict": true,
"strictNullChecks": false,
"strictPropertyInitialization": false,
"outDir": "./dist",
"rootDir": "./webapp",
"types": ["@sapui5/types", "@types/jquery", "@types/qunit"],
"paths": {
"com/myorg/myapp/*": ["./webapp/*"],
"unit/*": ["./webapp/test/unit/*"],
"integration/*": ["./webapp/test/integration/*"]
}
},
"exclude": ["./webapp/test/e2e/**/*"],
"include": ["./webapp/**/*"]
}ui5-tooling-transpile-taskui5-tooling-transpile-middlewarebuilder:
customTasks:
- name: ui5-tooling-transpile-task
afterTask: replaceVersion
server:
customMiddleware:
- name: ui5-tooling-transpile-middleware
afterMiddleware: compression
- name: ui5-middleware-livereload
afterMiddleware: compressionservereslint.config.mjsimport eslint from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
globals: {
...globals.browser,
sap: "readonly"
},
ecmaVersion: 2023,
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname
}
}
},
{
ignores: ["eslint.config.mjs"]
}
);SuperClass.extend(...)classextendextend[... other code, e.g. loading the dependencies "App", "Controller" etc. ...]
var App = Controller.extend("ui5tssampleapp.controller.App", {
onInit: function _onInit() {
// apply content density mode to root view
this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
}
});[... other code, e.g. loading the dependencies "App", "Controller" etc. ...]
/**
* @namespace ui5tssampleapp.controller
*/
class App extends Controller {
public onInit() {
// apply content density mode to root view
this.getView().addStyleClass((this.getOwnerComponent()).getContentDensityClass());
};
};sap.ui.require(...)sap.ui.define(...)sap.ui.definesap.ui.define(["sap/ui/core/mvc/Controller"], function (Controller) {
/**
* @namespace ui5tssampleapp.controller
*/
class App extends Controller {
... // as above
};
return App;
});import Controller from "sap/ui/core/mvc/Controller";
/**
* @namespace ui5tssampleapp.controller
*/
export default class App extends Controller {
... // as above
};sap.ui.requireHint: importingdoes not provide the class (like for most other UI5 modules), but the singleton instance of the UI5 Core. So the imported module can be used directly for methods likesap/ui/core/Coreinstead of calls tobyId(...)which return the singleton in JavaScript.sap.ui.getCore()
sap.ui.requiresap.ui.require(["sap/m/MessageBox"], function(MessageBox) { ... })import("sap/m/MessageBox").then((MessageBox) => { ... })functionsomeFunction.bind(...)IMPORTANT: whenever you use a UI5 type, e.g. for annotating a variable or method parameter/returntype, do NOT use the UI5 type with its global namespace (likeorsap.m.Button)! Instead, import this UI5 type from the respective module (likesap.ui.core.Popuporsap/m/Button- add an import if needed) and use the imported module.sap/ui/core/Popup
const b: sap.m.Button;
function getPopup(): sap.ui.core.Popup { ... }import Button from "sap/m/Button";
import Popup from "sap/ui/core/Popup";
const b: Button;
function getPopup(): Popup { ... }EventMouseEventButton$PressEventButton$PressEventParameterssap/m/Buttonpresssap/m/ButtonNote: for any event XYZ of a UI5 control ABC, types likeandABC$XYZEventare available!ABC$XYZEventParameters
sap.ui.define(["./BaseController"], function (BaseController) {
return BaseController.extend("my.app.controller.Main", {
onPress: function(oEvent) {
const button = oEvent.getSource();
},
onSelectionChange: function(oEvent) {
const items = oEvent.getParameter("selectedItems");
}
});
});import BaseController from "./BaseController";
import Button from "sap/m/Button";
import {Button$PressEvent} from "sap/m/Button";
import {Table$RowSelectionChangeEvent} from "sap/ui/table/Table";
export default class Main extends BaseController {
onPress(oEvent: Button$PressEvent): void {
const button = oEvent.getSource() as Button;
}
onRowSelectionChange(oEvent: Table$RowSelectionChangeEvent): void {
const selectedContext = oEvent.getParameter("rowContext");
}
}KeyboardEventMouseEventEventButton$PressEventsap/m/Buttonsap/ui/base/Eventdocument.getElementById(...)someUI5Control.getModel()this.byId()HTMLElementsap.ui.model.Modelsap.ui.core.ElementHTMLAnchorElementsap.ui.model.odata.v4.ODataModelsap.m.Inputsap.ui.model.ModelODataModelAppComponentonInitgetOwnerComponentsap.ui.core.ComponentgetContentDensityClassimport Controller from "sap/ui/core/mvc/Controller";
/**
* @namespace ui5tssampleapp.controller
*/
export default class App extends Controller {
public onInit() {
// apply content density mode to root view
this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
};
};import Controller from "sap/ui/core/mvc/Controller";
import AppComponent from "../Component";
/**
* @namespace ui5tssampleapp.controller
*/
export default class App extends Controller {
public onInit() : void {
// apply content density mode to root view
this.getView().addStyleClass((this.getOwnerComponent() as AppComponent).getContentDensityClass());
};
};This section covers the conversion of UI5 custom controls from JavaScript to TypeScript. This applies both to single custom controls within applications and to control libraries.
textstatic readonly metadata: MetadataOptions = {
properties: {
"text": "string"
}
};rm.text(control.getText()); // ERROR: Property 'getText' does not exist on type 'MyControl'new MyControl("myId", {text: "Hello"}); // TypeScript doesn't know about the settings object structuregetText()setText()bindText()addItem()removeItem()getItems()getLabel()setLabel()attachPress()detachPress()firePress()npm install --save-dev @ui5/ts-interface-generator@{{ts-interface-generator-version}}package.json{
"scripts": {
"watch:controls": "npx @ui5/ts-interface-generator --watch"
}
}tsconfig.json npx @ui5/ts-interface-generator --watch --config path/to/tsconfig.jsonnpm run watch:controls*.gen.d.tsMyControl.gen.d.ts===== BEGIN =====
// The following three lines were generated and should remain as-is to make TypeScript aware of the constructor signatures
constructor(id?: string | $MyControlSettings);
constructor(id?: string, settings?: $MyControlSettings);
constructor(id?: string, settings?: $MyControlSettings) { super(id, settings); }
===== END =====export default class MyControl extends Control {
// The following three lines were generated and should remain as-is to make TypeScript aware of the constructor signatures
constructor(id?: string | $MyControlSettings);
constructor(id?: string, settings?: $MyControlSettings);
constructor(id?: string, settings?: $MyControlSettings) { super(id, settings); }
static readonly metadata: MetadataOptions = {
// ...
};
}MetadataOptionsimport type { MetadataOptions } from "sap/ui/core/Element";
export default class MyControl extends Control {
static readonly metadata: MetadataOptions = {
properties: {
"text": "string"
}
};
}MetadataOptionssap/ui/core/Elementsap/ui/core/Objectsap/ui/core/ManagedObjectsap/ui/core/Componentimport typeimportMetadataOptionsobject@namespace/**
* @namespace ui5.typescript.helloworld.control
*/
export default class MyControl extends Control {
// ...
}export default// CORRECT:
export default class MyControl extends Control {
// ...
}
// WRONG - separate export:
class MyControl extends Control {
// ...
}
export default MyControl;staticimport RenderManager from "sap/ui/core/RenderManager";
export default class MyControl extends Control {
static readonly metadata: MetadataOptions = {
properties: {
"text": "string"
}
};
static renderer = {
apiVersion: 2,
render: function (rm: RenderManager, control: MyControl): void {
rm.openStart("div", control);
rm.openEnd();
rm.text(control.getText());
rm.close("div");
}
};
}sap.ui.define([
"sap/ui/core/Control",
"./MyControlRenderer"
], function (Control, MyControlRenderer) {
"use strict";
return Control.extend("com.myorg.myapp.control.MyControl", {
...
renderer: MyControlRenderer,
...import Control from "sap/ui/core/Control";
import type { MetadataOptions } from "sap/ui/core/Element";
import MyControlRenderer from "./MyControlRenderer";
/**
* @namespace com.myorg.myapp.control
*/
export default class MyControl extends Control {
...
static renderer = MyControlRenderer;
...sap.ui.define([
"sap/ui/core/Control",
"sap/ui/core/RenderManager"
], function (Control, RenderManager) {
"use strict";
var MyControl = Control.extend("ui5.typescript.helloworld.control.MyControl", {
metadata: {
properties: {
"text": "string"
},
events: {
"press": {}
}
},
renderer: function (rm, control) {
rm.openStart("div", control);
rm.openEnd();
rm.text(control.getText());
rm.close("div");
},
onclick: function() {
this.firePress();
}
});
return MyControl;
});import Control from "sap/ui/core/Control";
import type { MetadataOptions } from "sap/ui/core/Element";
import RenderManager from "sap/ui/core/RenderManager";
/**
* @namespace ui5.typescript.helloworld.control
*/
export default class MyControl extends Control {
// The following three lines were generated and should remain as-is to make TypeScript aware of the constructor signatures
constructor(id?: string | $MyControlSettings);
constructor(id?: string, settings?: $MyControlSettings);
constructor(id?: string, settings?: $MyControlSettings) { super(id, settings); }
static readonly metadata: MetadataOptions = {
properties: {
"text": "string"
},
events: {
"press": {}
}
};
static renderer = {
apiVersion: 2,
render: function (rm: RenderManager, control: MyControl): void {
rm.openStart("div", control);
rm.openEnd();
rm.text(control.getText());
rm.close("div");
}
};
onclick(): void {
this.firePress();
}
}library.tsimport ObjectPath from "sap/base/util/ObjectPath";
// Define enum as TypeScript enum
export enum ExampleColor {
Red = "Red",
Green = "Green",
Blue = "Blue"
}
// CRITICAL: Attach to global library object
const thisLib = ObjectPath.get("com.myorg.myui5lib") as {[key: string]: unknown};
thisLib.ExampleColor = ExampleColor;type: "com.myorg.myui5lib.ExampleColor"{
"compilerOptions": {
"paths": {
"com/myorg/mylib/*": ["./src/*"]
}
}
}@namespaceexport defaultMetadataOptionsstatic@ui5/ts-interface-generator