SAP / ui5-typescript

Tooling to enable TypeScript support in SAPUI5/OpenUI5 projects
https://sap.github.io/ui5-typescript
Apache License 2.0
201 stars 28 forks source link

Typed Views with TS Seem Impossible #467

Closed denislutz closed 1 month ago

denislutz commented 1 month ago

Hi dear SAPUI experts and thanks a lot for your work.

I am attempting to use typed views with TypeScript

Coming from react.js I want to leverage the full power of TypeScript without encountering any language breaks with XML by having a pure TS MVC cycle.

Despite following various guides and documentation, I continuously face issues when trying to implement typed views in my SAP UI5 project. Below are the core parts of my code, including the manifest.json, three view classes (App, Master, and Detail), and the Component initialization file.

Here is my current setup: Manifest.json:

{
    "_version": "1.47.0",
    "sap.app": {
        "id": "explorer.explorerfcl",
        "type": "application",
        "i18n": "i18n/i18n.properties",
        "title": "{{appTitle}}",
        "description": "{{appDescription}}",
        "applicationVersion": {
            "version": "${version}"
        },
        "resources": "resources.json",
        "dataSources": {
            "mainService": {
                "uri": "/odata/v2/file-management/",
                "type": "OData",
                "settings": {
                    "odataVersion": "2.0",
                    "localUri": "localService/metadata.xml"
                }
            }
        }
    },
    "sap.ui": {
        "fullWidth": true,
        "technology": "UI5",
        "icons": {
            "icon": "sap-icon://detail-view",
            "favIcon": "",
            "phone": "",
            "phone@2": "",
            "tablet": "",
            "tablet@2": ""
        },
        "deviceTypes": {
            "desktop": true,
            "tablet": true,
            "phone": true
        }
    },
    "sap.ui5": {
        "dependencies": {
            "minUI5Version": "1.90.0",
            "libs": {
                "sap.ui.core": {},
                "sap.m": {},
                "sap.f": {},
                "sap.ui.layout": {},
                "sap.ui.table": {}
            }
        },
        "contentDensities": {
            "compact": true,
            "cozy": true
        },
        "models": {
            "": {
                "dataSource": "mainService",
                "preload": true,
                "settings": {
                    "useBatch": false,
                    "defaultOperationMode": "Server",
                    "defaultBindingMode": "TwoWay",
                    "defaultCountMode": "Inline"
                }
            },
            "i18n": {
                "type": "sap.ui.model.resource.ResourceModel",
                "settings": {
                    "bundleName": "explorer.explorerfcl.i18n.i18n"
                }
            }
        },
        "resources": {
            "css": [
                {
                    "uri": "style/style.css"
                }
            ]
        },
        "routing": {
            "config": {
                "routerClass": "sap.f.routing.Router",
                "viewType": "JS",
                "viewPath": "explorer.explorerfcl.view",
                "controlId": "fcl",
                "controlAggregation": "beginColumnPages",
                "bypassed": {
                    "target": "master"
                },
                "async": true
            },
            "routes": [
                {
                    "pattern": ":layout:",
                    "name": "master",
                    "target": ["master"]
                },
                {
                    "pattern": "detail/{id}/{layout}",
                    "name": "detail",
                    "target": ["master", "detail"]
                }
            ],
            "targets": {
                "master": {
                    "viewName": "Master",
                    "viewLevel": 1,
                    "viewId": "master",

                },
                "detail": {
                    "viewName": "Detail",
                    "viewId": "detail",
                    "viewLevel": 1,
                    "controlAggregation": "midColumnPages",

                },
                "detailObjectNotFound": {
                    "viewName": "DetailObjectNotFound",
                    "viewId": "detailObjectNotFound",
                    "controlAggregation": "midColumnPages",

                }
            }
        }
    }
}
import View from "sap/ui/core/mvc/View";
import App from "sap/m/App";
import FlexibleColumnLayout from "sap/f/FlexibleColumnLayout";
import Text from "sap/m/Text";
import Control from "sap/ui/core/Control";

export default class AppView extends View {
    public getControllerName(): string {
        return "explorer.explorerfcl.controller.App";
    }

    public createContent(): Control {
        return new App({
            id: this.createId("app"),
            busy: "{appView>/busy}",
            busyIndicatorDelay: "{appView>/delay}",
            pages: [
                new FlexibleColumnLayout("fcl", {
                    layout: "{appView>/layout}",
                    backgroundDesign: "Translucent",
                    beginColumnPages: [new Text({ text: "Begin Column Page Content" })],
                    midColumnPages: [new Text({ text: "Mid Column Page Content" })],
                    endColumnPages: [new Text({ text: "End Column Page Content" })]
                })
            ]
        });
    }
}
import View from "sap/ui/core/mvc/View";
import DynamicPage from "sap/f/DynamicPage";
import Title from "sap/m/Title";
import DynamicPageTitle from "sap/f/DynamicPageTitle";
import Text from "sap/m/Text";
import Control from "sap/ui/core/Control";

export default class MasterView extends View {
    public getControllerName(): string {
        return "explorer.explorerfcl.controller.Master";
    }

    public createContent(): Control {
        return new DynamicPage("masterPage", {
            stickySubheaderProvider: "iconTabBar",
            title: new DynamicPageTitle({
                heading: new Title({
                    text: "{i18n>appTitle}",
                    level: "H2"
                })
            }),
            content: new Text({ text: "Master Page Content" })
        });
    }
}
import View from "sap/ui/core/mvc/View";
import DynamicPage from "sap/f/DynamicPage";
import Title from "sap/m/Title";
import DynamicPageTitle from "sap/f/DynamicPageTitle";
import Text from "sap/m/Text";
import Control from "sap/ui/core/Control";

export default class DetailView extends View {
    public getControllerName(): string {
        return "explorer.explorerfcl.controller.Detail";
    }

    public createContent(): Control {
        return new DynamicPage("detailPage", {
            showFooter: "{comp>/detailEditMode}",
            stickySubheaderProvider: "iconTabBar",
            busy: "{detailView>/busy}",
            busyIndicatorDelay: "{detailView>/delay}",
            title: new DynamicPageTitle({
                heading: new Title({
                    text: "{designNumber}",
                    level: "H2"
                })
            }),
            content: new Text({ text: "Detail Page Content" })
        });
    }
}
import UIComponent from "sap/ui/core/UIComponent";
import View from "sap/ui/core/mvc/View";
import ViewType from "sap/ui/core/mvc/ViewType";
import JSONModel from "sap/ui/model/json/JSONModel";
import ErrorHandler from "./ErrorHandler";
import models from "./model/models";

export default class Component extends UIComponent {
    private errorHandler: ErrorHandler;

    public static metadata = {
        manifest: "json"
    };

    public init(): void {
        this.errorHandler = new ErrorHandler(this);
        super.init();
        this.setModel(models.createDeviceModel(), "device");
        this.setModel(new JSONModel(), "appView");

        this.getRouter().attachBeforeRouteMatched(this.onBeforeRouteMatched, this);
        this.getRouter().initialize();

// Usually I was not creating the views separatelly, with this method, but due to warnings also added this here.
        this._createViews();
    }

    private async _createViews(): Promise<void> {
        try {
            const appView = await View.create({
                viewName: "module:explorer.explorerfcl.view.App",
                type: ViewType.JS
            });
            appView.placeAt("content");

            await View.create({
                viewName: "module:explorer.explorerfcl.view.Master",
                type: ViewType.JS
            });

            await View.create({
                viewName: "module:explorer.explorerfcl.view.Detail",
                type: ViewType.JS
            });
        } catch (error) {
            console.error("Error creating views:", error);
        }
    }

    private onBeforeRouteMatched(event: any): void {
        // Handle route matched events
    }
}

The errors I am getting are always the same:

Warnings

2024-07-19 01:11:20.704000 Do not use deprecated sap.ui.core.mvc.JSView: (View: __component0---master). Use typed views defined by 'sap.ui.core.mvc.View.extend()' and created by 'sap.ui.core.mvc.View.create()'. For further information, have a look at https://sdk.openui5.org/topic/e6bb33d076dc4f23be50c082c271b9f0. - sap.ui.core.mvc.JSView 

Errors


ModuleError: failed to load 'explorer.explorerfcl.view.App.js' from resources/explorer.explorerfcl.view.App.js: script load error
makeModuleError ui5loader.js:1190
failWith ui5loader.js:958
onerror ui5loader.js:1487
loadScript ui5loader.js:1495
onerror ui5loader.js:1482

2024-07-19 01:11:20.736000 [FUTURE FATAL] Failed to load component for container __container0 - TypeError: this.createContent is not a function  
TypeError: this.createContent is not a function
    onControllerConnected JS

Do you see what I am doing, wrong? Thanks a lot in advance.

codeworrior commented 1 month ago

The warning "Do not use deprecated sap.ui.core.mvc.JSView:" and the error "failed to load 'explorer.explorerfcl.view.App.js' from..." indicate what's going wrong.

When creating your typed view, you should refer to it with the "module:" prefix, followed by the module ID - which is a slash separated name, not dot separated. I guess loading your view fails due to this and the runtime falls back to a JSView (btw: you can and should remove the type: ViewType.JS setting. The use of the "module:" prefix already implies a typed view).

Please try that and fix the view names.

BTW: If my above assumption is correct, I would have expected a different (more helpful) error message. Something that indicates that the view could not be found. The silent fallback to JSView surprises me.

denislutz commented 1 month ago

Hello Frank,

thanks a lot for your reply. Now I tried for several hours to apply your input, I created a fresh and very clean TS sample but it is still not working. My general feeling as a SAPUI new comer, that there are too many places where I can do something wrong in configuration. Also I feel like the framework expects it all to be compiled in .js version but my TS code is missing something to deliver it.

My last error is this:

2024-07-19 12:06:07.815000 [FUTURE FATAL] Failed to load component for container __container0 - ModuleError: failed to load 'com/dl/explorer/view/App.js' from ./view/App.js: 404 - Not Found ModuleError: failed to load 'com/dl/explorer/view/App.js' from ./view/App.js: 404 - Not Found makeModuleError http://localhost:8080/resources/ui5loader.js:1190 failWith http://localhost:8080/resources/ui5loader.js:958 requireModule http://localhost:8080/resources/ui5loader.js:1692 requireSync http://localhost:8080/resources/ui5loader.js:2228 createView http://localhost:8080/resources/sap/ui/core/mvc/View.js:1327 createView http://localhost:8080/resources/sap/ui/core/mvc/View.js:1332 viewFactory http://localhost:8080/resources/sap/ui/core/mvc/View.js:1266 createContent http://localhost:8080/resources/sap/ui/core/UIComponent.js:773 init http://localhost:8080/resources/sap/ui/core/UIComponent.js:394 runWithPreprocessors http://localhost:8080/resources/sap/ui/base/ManagedObject.js?eval:1189 init http://localhost:8080/resources/sap/ui/core/UIComponent.js:393 runWithOwner http://localhost:8080/resources/sap/ui/base/ManagedObject.js?eval:1216 Caused by: XHRLoadError: 404 - Not Found createXHRLoadError ui5loader.js:1403 loadSyncXHR ui5loader.js:1416 callHandler syncXHRFix.js:149 fnWrappedHandler syncXHRFix.js:161 get syncXHRFix.js:222 loadSyncXHR ui5loader.js:1426 Here my full project that you can just replicate what I have. If you do take a look at the Component.ts as the first step, there I deactivated the creation of the views. com.dl.explorer.zip

I would be very nice if you could provide a very basic sample of working typed views with TS, there is not a single show case on the whole net that demonstrates it.

codeworrior commented 1 month ago

Just one minor thing is wrong: your view is named view/App.view.js . The module ID therefore is "<your app namespace>/view/App.view". The ".view" part is missing in your manifest.json, both, for the root view and for the views in the routing section.

After fixing that, I see the product list.

Clicking on an item in the product list revels another issue: the event handler is not bound to the controller. Typed views are just JavaScript, so if you assign a function reference as event handler to a Button's press event, the button cannot know where this reference came from (the controller instance). in XMLViews, the framework knows the relationship and can bind the function accordingly. In a typed view, you have to compensate this:

    press: (this.getController() as Master).onListItemPress.bind(this.getController()),

After that, the app work as intended, as far as I can judge this.

denislutz commented 1 month ago

Hi Frank,

wow it works now, thanks so much for your support. What a nice start in the week end.

Here is the adapted version, for everyone who comes for this issue to this thread: com.dl.explorer.zip

Grüsse aus Mannheim!

denislutz commented 1 month ago

Answer complete