├── .editorconfig
├── .gitignore
├── .markdownlint.json
├── LICENSE
├── LICENSES
└── Apache-2.0.txt
├── README.md
├── REUSE.toml
├── exercises
├── ex0
│ └── README.md
├── ex1
│ ├── README.md
│ └── com.myorg.myapp
│ │ ├── .editorconfig
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── .yo-rc.json
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── karma-ci-cov.conf.js
│ │ ├── karma-ci.conf.js
│ │ ├── karma.conf.js
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── ui5-dist.yaml
│ │ ├── ui5.yaml
│ │ └── webapp
│ │ ├── Component.ts
│ │ ├── controller
│ │ ├── App.controller.ts
│ │ ├── BaseController.ts
│ │ └── Main.controller.ts
│ │ ├── i18n
│ │ ├── i18n.properties
│ │ ├── i18n_de.properties
│ │ └── i18n_en.properties
│ │ ├── index-cdn.html
│ │ ├── index.html
│ │ ├── manifest.json
│ │ ├── model
│ │ ├── formatter.ts
│ │ └── models.ts
│ │ ├── test
│ │ ├── Test.qunit.html
│ │ ├── integration
│ │ │ ├── HelloJourney.ts
│ │ │ ├── opaTests.qunit.ts
│ │ │ └── pages
│ │ │ │ └── MainPage.ts
│ │ ├── testsuite.qunit.html
│ │ ├── testsuite.qunit.ts
│ │ └── unit
│ │ │ ├── controller
│ │ │ └── Main.qunit.ts
│ │ │ └── unitTests.qunit.ts
│ │ └── view
│ │ ├── App.view.xml
│ │ └── Main.view.xml
├── ex2
│ ├── README.md
│ ├── com.myorg.myapp
│ │ ├── .editorconfig
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── .yo-rc.json
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── karma-ci-cov.conf.js
│ │ ├── karma-ci.conf.js
│ │ ├── karma.conf.js
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── ui5-dist.yaml
│ │ ├── ui5.yaml
│ │ └── webapp
│ │ │ ├── Component.ts
│ │ │ ├── controller
│ │ │ ├── App.controller.ts
│ │ │ ├── BaseController.ts
│ │ │ └── Main.controller.ts
│ │ │ ├── i18n
│ │ │ ├── i18n.properties
│ │ │ ├── i18n_de.properties
│ │ │ └── i18n_en.properties
│ │ │ ├── index-cdn.html
│ │ │ ├── index.html
│ │ │ ├── manifest.json
│ │ │ ├── model
│ │ │ ├── formatter.ts
│ │ │ └── models.ts
│ │ │ ├── test
│ │ │ ├── Test.qunit.html
│ │ │ ├── integration
│ │ │ │ ├── HelloJourney.ts
│ │ │ │ ├── opaTests.qunit.ts
│ │ │ │ └── pages
│ │ │ │ │ └── MainPage.ts
│ │ │ ├── testsuite.qunit.html
│ │ │ ├── testsuite.qunit.ts
│ │ │ └── unit
│ │ │ │ ├── controller
│ │ │ │ └── Main.qunit.ts
│ │ │ │ └── unitTests.qunit.ts
│ │ │ └── view
│ │ │ ├── App.view.xml
│ │ │ └── Main.view.xml
│ └── images
│ │ ├── codeassist_jsonmodel.png
│ │ ├── create_onInit.png
│ │ ├── ex2.png
│ │ ├── hover_init.png
│ │ ├── jsonmodel_error.png
│ │ └── jsonmodel_quickfix.png
├── ex3
│ ├── README.md
│ ├── com.myorg.myapp
│ │ ├── .editorconfig
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── .yo-rc.json
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── karma-ci-cov.conf.js
│ │ ├── karma-ci.conf.js
│ │ ├── karma.conf.js
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── ui5-dist.yaml
│ │ ├── ui5.yaml
│ │ └── webapp
│ │ │ ├── Component.ts
│ │ │ ├── controller
│ │ │ ├── App.controller.ts
│ │ │ ├── BaseController.ts
│ │ │ └── Main.controller.ts
│ │ │ ├── i18n
│ │ │ ├── i18n.properties
│ │ │ ├── i18n_de.properties
│ │ │ └── i18n_en.properties
│ │ │ ├── index-cdn.html
│ │ │ ├── index.html
│ │ │ ├── manifest.json
│ │ │ ├── model
│ │ │ ├── formatter.ts
│ │ │ └── models.ts
│ │ │ ├── test
│ │ │ ├── Test.qunit.html
│ │ │ ├── integration
│ │ │ │ ├── HelloJourney.ts
│ │ │ │ ├── opaTests.qunit.ts
│ │ │ │ └── pages
│ │ │ │ │ └── MainPage.ts
│ │ │ ├── testsuite.qunit.html
│ │ │ ├── testsuite.qunit.ts
│ │ │ └── unit
│ │ │ │ ├── controller
│ │ │ │ └── Main.qunit.ts
│ │ │ │ └── unitTests.qunit.ts
│ │ │ └── view
│ │ │ ├── App.view.xml
│ │ │ └── Main.view.xml
│ └── images
│ │ ├── event_parameter_any_error.png
│ │ ├── event_parameters_content_assist.png
│ │ ├── ex3.png
│ │ └── wrong_event.png
├── ex4
│ ├── README.md
│ ├── com.myorg.myapp
│ │ ├── .editorconfig
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── .yo-rc.json
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── karma-ci-cov.conf.js
│ │ ├── karma-ci.conf.js
│ │ ├── karma.conf.js
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── ui5-dist.yaml
│ │ ├── ui5.yaml
│ │ └── webapp
│ │ │ ├── Component.ts
│ │ │ ├── controller
│ │ │ ├── App.controller.ts
│ │ │ ├── BaseController.ts
│ │ │ └── Main.controller.ts
│ │ │ ├── i18n
│ │ │ ├── i18n.properties
│ │ │ ├── i18n_de.properties
│ │ │ └── i18n_en.properties
│ │ │ ├── index-cdn.html
│ │ │ ├── index.html
│ │ │ ├── manifest.json
│ │ │ ├── model
│ │ │ ├── formatter.ts
│ │ │ └── models.ts
│ │ │ ├── test
│ │ │ ├── Test.qunit.html
│ │ │ ├── integration
│ │ │ │ ├── HelloJourney.ts
│ │ │ │ ├── opaTests.qunit.ts
│ │ │ │ └── pages
│ │ │ │ │ └── MainPage.ts
│ │ │ ├── testsuite.qunit.html
│ │ │ ├── testsuite.qunit.ts
│ │ │ └── unit
│ │ │ │ ├── controller
│ │ │ │ └── Main.qunit.ts
│ │ │ │ └── unitTests.qunit.ts
│ │ │ └── view
│ │ │ ├── App.view.xml
│ │ │ └── Main.view.xml
│ └── images
│ │ ├── ex4.png
│ │ ├── nominatim_createClient.png
│ │ └── placeName_error.png
├── ex5
│ ├── README.md
│ ├── com.myorg.myapp
│ │ ├── .editorconfig
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── .yo-rc.json
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── karma-ci-cov.conf.js
│ │ ├── karma-ci.conf.js
│ │ ├── karma.conf.js
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── ui5-dist.yaml
│ │ ├── ui5.yaml
│ │ └── webapp
│ │ │ ├── Component.ts
│ │ │ ├── control
│ │ │ ├── WindDirection.gen.d.ts
│ │ │ └── WindDirection.ts
│ │ │ ├── controller
│ │ │ ├── App.controller.ts
│ │ │ ├── BaseController.ts
│ │ │ └── Main.controller.ts
│ │ │ ├── i18n
│ │ │ ├── i18n.properties
│ │ │ ├── i18n_de.properties
│ │ │ └── i18n_en.properties
│ │ │ ├── index-cdn.html
│ │ │ ├── index.html
│ │ │ ├── manifest.json
│ │ │ ├── model
│ │ │ ├── formatter.ts
│ │ │ └── models.ts
│ │ │ ├── test
│ │ │ ├── Test.qunit.html
│ │ │ ├── integration
│ │ │ │ ├── HelloJourney.ts
│ │ │ │ ├── opaTests.qunit.ts
│ │ │ │ └── pages
│ │ │ │ │ └── MainPage.ts
│ │ │ ├── testsuite.qunit.html
│ │ │ ├── testsuite.qunit.ts
│ │ │ └── unit
│ │ │ │ ├── controller
│ │ │ │ └── Main.qunit.ts
│ │ │ │ └── unitTests.qunit.ts
│ │ │ └── view
│ │ │ ├── App.view.xml
│ │ │ └── Main.view.xml
│ └── images
│ │ ├── Constructor_Suggestions.png
│ │ └── getDirection_error.png
└── ex6
│ ├── README.md
│ └── com.myorg.myapp
│ ├── .editorconfig
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── .yo-rc.json
│ ├── LICENSE
│ ├── README.md
│ ├── karma-ci-cov.conf.js
│ ├── karma-ci.conf.js
│ ├── karma.conf.js
│ ├── package-lock.json
│ ├── package.json
│ ├── tsconfig.json
│ ├── ui5-dist.yaml
│ ├── ui5.yaml
│ └── webapp
│ ├── Component.ts
│ ├── control
│ ├── WindDirection.gen.d.ts
│ └── WindDirection.ts
│ ├── controller
│ ├── App.controller.ts
│ ├── BaseController.ts
│ └── Main.controller.ts
│ ├── i18n
│ ├── i18n.properties
│ ├── i18n_de.properties
│ └── i18n_en.properties
│ ├── index-cdn.html
│ ├── index.html
│ ├── manifest.json
│ ├── model
│ ├── formatter.ts
│ └── models.ts
│ ├── test
│ ├── Test.qunit.html
│ ├── integration
│ │ ├── HelloJourney.ts
│ │ ├── opaTests.qunit.ts
│ │ └── pages
│ │ │ └── MainPage.ts
│ ├── testsuite.qunit.html
│ ├── testsuite.qunit.ts
│ └── unit
│ │ ├── control
│ │ └── WindDirection.qunit.ts
│ │ ├── controller
│ │ └── Main.qunit.ts
│ │ └── unitTests.qunit.ts
│ └── view
│ ├── App.view.xml
│ └── Main.view.xml
└── generator
├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .lintstagedrc.json
├── .prettierignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── generators
└── app
│ ├── index.js
│ └── templates
│ ├── LICENSE
│ ├── README.md
│ ├── _.editorconfig
│ ├── _.eslintrc.js
│ ├── _.gitignore
│ ├── karma-ci-cov.conf.js
│ ├── karma-ci.conf.js
│ ├── karma.conf.js
│ ├── package.json
│ ├── tsconfig.json
│ ├── ui5-dist.yaml
│ ├── ui5.yaml
│ └── webapp
│ ├── Component.ts
│ ├── controller
│ ├── App.controller.ts
│ ├── BaseController.ts
│ └── Main.controller.ts
│ ├── i18n
│ ├── i18n.properties
│ ├── i18n_de.properties
│ └── i18n_en.properties
│ ├── index-cdn.html
│ ├── index.html
│ ├── manifest.json
│ ├── model
│ ├── formatter.ts
│ └── models.ts
│ ├── test
│ ├── Test.qunit.html
│ ├── integration
│ │ ├── HelloJourney.ts
│ │ ├── opaTests.qunit.ts
│ │ └── pages
│ │ │ └── MainPage.ts
│ ├── testsuite.qunit.html
│ ├── testsuite.qunit.ts
│ └── unit
│ │ ├── controller
│ │ └── Main.qunit.ts
│ │ └── unitTests.qunit.ts
│ └── view
│ ├── App.view.xml
│ └── Main.view.xml
├── package-lock.json
└── package.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 |
7 | [*.{css,html,ts,js,xml,less,txt}]
8 | trim_trailing_whitespace = true
9 | end_of_line = lf
10 | tab_width = 4
11 |
12 | [*.{json,yaml}]
13 | indent_style = space
14 | indent_size = 2
15 |
16 | [*.md]
17 | trim_trailing_whitespace = false
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | node_modules
3 |
4 | .DS_Store
5 | 2022
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "MD010": {
3 | "code_blocks": false
4 | },
5 | "MD013": false
6 | }
--------------------------------------------------------------------------------
/REUSE.toml:
--------------------------------------------------------------------------------
1 | version = 1
2 | SPDX-PackageName = "ui5-typescript-tutorial"
3 | SPDX-PackageSupplier = "OpenUI5 Team (openui5@sap.com)"
4 | SPDX-PackageDownloadLocation = "https://github.com/SAP-samples/ui5-typescript-tutorial"
5 | SPDX-PackageComment = "The code in this project may include calls to APIs (“API Calls”) of\n SAP or third-party products or services developed outside of this project\n (“External Products”).\n “APIs” means application programming interfaces, as well as their respective\n specifications and implementing code that allows software to communicate with\n other software.\n API Calls to External Products are not licensed under the open source license\n that governs this project. The use of such API Calls and related External\n Products are subject to applicable additional agreements with the relevant\n provider of the External Products. In no event shall the open source license\n that governs this project grant any rights in or to any External Products,or\n alter, expand or supersede any terms of the applicable additional agreements.\n If you have a valid license agreement with SAP for the use of a particular SAP\n External Product, then you may make use of any API Calls included in this\n project’s code for that SAP External Product, subject to the terms of such\n license agreement. If you do not have a valid license agreement for the use of\n a particular SAP External Product, then you may only make use of any API Calls\n in this project for that SAP External Product for your internal, non-productive\n and non-commercial test and evaluation of such API Calls. Nothing herein grants\n you any rights to use or access any SAP External Product, or provide any third\n parties the right to use of access any SAP External Product, through API Calls."
6 |
7 | [[annotations]]
8 | path = "**"
9 | precedence = "aggregate"
10 | SPDX-FileCopyrightText = "2023 SAP SE or an SAP affiliate company and ui5-typescript-tutorial contributors"
11 | SPDX-License-Identifier = "Apache-2.0"
12 |
--------------------------------------------------------------------------------
/exercises/ex0/README.md:
--------------------------------------------------------------------------------
1 | # Exercise 0 - Get Prepared
2 |
3 | To get started, you need to ensure that you have a recent [Node.js](https://nodejs.org/de/download) version (recommended: 18.x or 20.x) installed on your machine.
4 |
5 | ## Install Yeoman And Easy UI5
6 |
7 | Next to [Node.js](https://nodejs.org/de/download) you need to install [Yeoman](https://yeoman.io/) and the [Easy UI5 Generator](https://github.com/SAP/generator-easy-ui5). Both NPM packages will be installed globally by entering the following command in your console:
8 |
9 | ```sh
10 | npm install -g yo generator-easy-ui5
11 | ```
12 |
13 | Please verify your installation to see if Yeoman has been installed correctly and the Easy UI5 Generator is available by executing the following command in your console:
14 |
15 | ```sh
16 | yo --generators
17 | ```
18 |
19 | Make sure that `easy-ui5` is listed.
20 |
21 | To verify the version of the installed generator-easy-ui5 you can run the following command:
22 |
23 | ```sh
24 | npm list -g "generator-easy-ui5"
25 | ```
26 |
27 | > :warning: **Remark:** The version must be at least **```3.6.2```** to be able to consume the latest template from this repository available [here](https://github.com/SAP-samples/ui5-typescript-tutorial/tree/main/generator)!
28 | > When using Node.js 21.x or higher, you need at least version 3.7.0 of the Easy UI5 Generator.
29 | > For the tutorial we used the Easy UI5 Generator 3.6.2. You can also explicitly install this version via `npm install -g generator-easy-ui5@3.6.2`.
30 |
31 | ## Summary
32 |
33 | Now that you have prepared your computer you can go ahead and create your first UI5 TypeScript application.
34 |
35 | Continue to - [Exercise 1 - Create And Run Your Application](../ex1/README.md)
36 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | # We recommend you to keep these unchanged
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 |
14 | # Change these settings to your own preference
15 | indent_style = tab
16 | indent_size = 2
17 |
18 | [*.{yaml,yml}]
19 | indent_style = space
20 |
21 | [*.md]
22 | indent_style = unset
23 | trim_trailing_whitespace = false
24 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "root": true,
3 | "env": {
4 | "browser": true,
5 | "es6": true,
6 | "node": true
7 | },
8 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking"],
9 | "parser": "@typescript-eslint/parser",
10 | "parserOptions": {
11 | "project": [`${__dirname}/tsconfig.json`],
12 | "sourceType": "module"
13 | },
14 | "plugins": ["@typescript-eslint"],
15 | "ignorePatterns": [".eslintrc.js"]
16 | };
17 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/.gitignore:
--------------------------------------------------------------------------------
1 | # build results
2 | dist
3 | coverage
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 |
12 | # Dependency directories
13 | node_modules/
14 |
15 | .DS_Store
16 | .env
17 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/.yo-rc.json:
--------------------------------------------------------------------------------
1 | {
2 | "generator-ui5-ts-app": {
3 | "namespace": "com.myorg.myapp",
4 | "framework": "OpenUI5",
5 | "frameworkVersion": "1.115.1",
6 | "author": "OpenUI5 Team",
7 | "initrepo": false,
8 | "tstypes": "@openui5/types",
9 | "tstypesVersion": "1.115.1",
10 | "appId": "com.myorg.myapp",
11 | "appURI": "com/myorg/myapp",
12 | "cdnDomain": "sdk.openui5.org",
13 | "defaultTheme": "sap_horizon",
14 | "gte11150": true,
15 | "setupCompleted": true
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/README.md:
--------------------------------------------------------------------------------
1 | # UI5 Application com.myorg.myapp
2 |
3 | Insert the purpose of this project and some interesting info here...
4 |
5 | ## Description
6 |
7 | This app demonstrates a TypeScript setup for developing UI5 applications. The central entry point for all information about using TypeScript with UI5 is at [https://sap.github.io/ui5-typescript](https://sap.github.io/ui5-typescript).
8 |
9 | **The template is inspired by the [`SAP-samples/ui5-typescript-helloworld`](https://github.com/SAP-samples/ui5-typescript-helloworld) project which also contains [a detailed step-by-step guide](https://github.com/SAP-samples/ui5-typescript-helloworld/blob/main/step-by-step.md). It explains how this setup is created and how all the bits and pieces fit together.**
10 |
11 | ## Requirements
12 |
13 | Either [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/) for dependency management.
14 |
15 | ## Preparation
16 |
17 | Use `npm` (or `yarn`) to install the dependencies:
18 |
19 | ```sh
20 | npm install
21 | ```
22 |
23 | (To use yarn, just do `yarn` instead.)
24 |
25 | ## Run the App
26 |
27 | Execute the following command to run the app locally for development in watch mode (the browser reloads the app automatically when there are changes in the source code):
28 |
29 | ```sh
30 | npm start
31 | ```
32 |
33 | As shown in the terminal after executing this command, the app is then running on http://localhost:8080/index.html. A browser window with this URL should automatically open.
34 |
35 | (When using yarn, do `yarn start` instead.)
36 |
37 | ## Debug the App
38 |
39 | In the browser, you can directly debug the original TypeScript code, which is supplied via sourcemaps (need to be enabled in the browser's developer console if it does not work straight away). If the browser doesn't automatically jump to the TypeScript code when setting breakpoints, use e.g. `Ctrl`/`Cmd` + `P` in Chrome to open the `*.ts` file you want to debug.
40 |
41 | ## Build the App
42 |
43 | ### Unoptimized (but quick)
44 |
45 | Execute the following command to build the project and get an app that can be deployed:
46 |
47 | ```sh
48 | npm run build
49 | ```
50 |
51 | The result is placed into the `dist` folder. To start the generated package, just run
52 |
53 | ```sh
54 | npm run start:dist
55 | ```
56 |
57 | Note that `index.html` still loads the UI5 framework from the relative URL `resources/...`, which does not physically exist, but is only provided dynamically by the UI5 tooling. So for an actual deployment you should change this URL to either [the CDN](https://sdk.openui5.org/#/topic/2d3eb2f322ea4a82983c1c62a33ec4ae) or your local deployment of UI5.
58 |
59 | (When using yarn, do `yarn build` and `yarn start:dist` instead.)
60 |
61 | ### Optimized
62 |
63 | For an optimized self-contained build (takes longer because the UI5 resources are built, too), do:
64 |
65 | ```sh
66 | npm run build:opt
67 | ```
68 |
69 | To start the generated package, again just run:
70 |
71 | ```sh
72 | npm run start:dist
73 | ```
74 |
75 | In this case, all UI5 framework resources are also available within the `dist` folder, so the folder can be deployed as-is to any static web server, without changing the bootstrap URL.
76 |
77 | With the self-contained build, the bootstrap URL in `index.html` has already been modified to load the newly created `sap-ui-custom.js` for bootstrapping, which contains all app resources as well as all needed UI5 JavaScript resources. Most UI5 resources inside the `dist` folder are for this reason actually **not** needed to run the app. Only the non-JS-files, like translation texts and CSS files, are used and must also be deployed. (Only when for some reason JS files are missing from the optimized self-contained bundle, they are also loaded separately.)
78 |
79 | (When using yarn, do `yarn build:opt` and `yarn start:dist` instead.)
80 |
81 | ## Check the Code
82 |
83 | Do the following to run a TypeScript check:
84 |
85 | ```sh
86 | npm run ts-typecheck
87 | ```
88 |
89 | This checks the application code for any type errors (but will also complain in case of fundamental syntax issues which break the parsing).
90 |
91 | To lint the TypeScript code, do:
92 |
93 | ```sh
94 | npm run lint
95 | ```
96 |
97 | (Again, when using yarn, do `yarn ts-typecheck` and `yarn lint` instead.)
98 |
99 | ## License
100 |
101 | This project is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](LICENSE) file.
102 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/karma-ci-cov.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | require("./karma-ci.conf")(config);
3 | config.set({
4 | reporters: ["progress", "coverage"],
5 | preprocessors: {
6 | "webapp/**/*.ts": ["ui5-transpile"]
7 | },
8 | coverageReporter: {
9 | dir: "coverage",
10 | reporters: [
11 | { type: "html", subdir: "report-html" },
12 | { type: "cobertura", subdir: ".", file: "cobertura.txt" },
13 | { type: "lcovonly", subdir: ".", file: "report-lcovonly.txt" },
14 | { type: "text-summary" }
15 | ]
16 | }
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/karma-ci.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | require("./karma.conf")(config);
3 | config.set({
4 | browsers: ["ChromeHeadless"],
5 | singleRun: true
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/karma.conf.js:
--------------------------------------------------------------------------------
1 | // karma-ui5 usage: https://github.com/SAP/karma-ui5
2 | module.exports = function (config) {
3 | config.set({
4 | frameworks: ["ui5"],
5 | browsers: ["Chrome"]
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "com.myorg.myapp",
3 | "version": "1.0.0",
4 | "description": "UI5 Application: com.myorg.myapp",
5 | "author": "OpenUI5 Team",
6 | "license": "Apache-2.0",
7 | "scripts": {
8 | "clean": "rimraf dist coverage",
9 | "build": "ui5 build --clean-dest",
10 | "build:opt": "ui5 build self-contained --clean-dest --all",
11 | "start": "ui5 serve --port 8080 -o index.html",
12 | "start-cdn": "ui5 serve --port 8080 -o index-cdn.html",
13 | "start:dist": "npm start -- --config ui5-dist.yaml",
14 | "start:dist-cdn": "npm run start-cdn -- --config ui5-dist.yaml",
15 | "ts-typecheck": "tsc --noEmit",
16 | "lint": "eslint webapp",
17 | "karma": "karma start",
18 | "karma-ci": "karma start karma-ci.conf.js",
19 | "karma-ci-cov": "karma start karma-ci-cov.conf.js",
20 | "test": "npm run lint && npm run karma-ci-cov"
21 | },
22 | "devDependencies": {
23 | "@openui5/types": "1.131.1",
24 | "@typescript-eslint/eslint-plugin": "^5.61.0",
25 | "@typescript-eslint/parser": "^5.61.0",
26 | "@ui5/cli": "^4.0.11",
27 | "eslint": "^8.44.0",
28 | "karma": "^6.4.2",
29 | "karma-chrome-launcher": "^3.2.0",
30 | "karma-coverage": "^2.2.1",
31 | "karma-ui5": "^3.0.3",
32 | "karma-ui5-transpile": "^0.3.22",
33 | "rimraf": "^5.0.1",
34 | "typescript": "^5.1.6",
35 | "ui5-middleware-livereload": "^0.8.4",
36 | "ui5-tooling-transpile": "^0.7.17"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2022",
4 | "module": "es2022",
5 | "moduleResolution": "node",
6 | "skipLibCheck": true,
7 | "allowJs": true,
8 | "strict": true,
9 | "strictNullChecks": false,
10 | "strictPropertyInitialization": false,
11 | "rootDir": "./webapp",
12 | "types": ["@openui5/types", "@types/qunit"],
13 | "paths": {
14 | "com/myorg/myapp/*": ["./webapp/*"],
15 | "unit/*": ["./webapp/test/unit/*"],
16 | "integration/*": ["./webapp/test/integration/*"]
17 | }
18 | },
19 | "include": ["./webapp/**/*"],
20 | "exclude": ["./webapp/coverage/**/*"]
21 | }
22 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/ui5-dist.yaml:
--------------------------------------------------------------------------------
1 | specVersion: "3.0"
2 | metadata:
3 | name: com.myorg.myapp
4 | type: application
5 | resources:
6 | configuration:
7 | paths:
8 | webapp: dist
9 | framework:
10 | name: OpenUI5
11 | version: "1.131.1"
12 | libraries:
13 | - name: sap.m
14 | - name: sap.ui.core
15 | - name: themelib_sap_horizon
16 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/ui5.yaml:
--------------------------------------------------------------------------------
1 | specVersion: "3.0"
2 | metadata:
3 | name: com.myorg.myapp
4 | type: application
5 | framework:
6 | name: OpenUI5
7 | version: "1.131.1"
8 | libraries:
9 | - name: sap.m
10 | - name: sap.ui.core
11 | - name: themelib_sap_horizon
12 | builder:
13 | customTasks:
14 | - name: ui5-tooling-transpile-task
15 | afterTask: replaceVersion
16 | server:
17 | customMiddleware:
18 | - name: ui5-tooling-transpile-middleware
19 | afterMiddleware: compression
20 | - name: ui5-middleware-livereload
21 | afterMiddleware: compression
22 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/Component.ts:
--------------------------------------------------------------------------------
1 | import UIComponent from "sap/ui/core/UIComponent";
2 | import models from "./model/models";
3 | import Device from "sap/ui/Device";
4 |
5 | /**
6 | * @namespace com.myorg.myapp
7 | */
8 | export default class Component extends UIComponent {
9 | public static metadata = {
10 | manifest: "json",
11 | };
12 |
13 | private contentDensityClass: string;
14 |
15 | public init(): void {
16 | // call the base component's init function
17 | super.init();
18 |
19 | // create the device model
20 | this.setModel(models.createDeviceModel(), "device");
21 |
22 | // create the views based on the url/hash
23 | this.getRouter().initialize();
24 | }
25 |
26 | /**
27 | * This method can be called to determine whether the sapUiSizeCompact or sapUiSizeCozy
28 | * design mode class should be set, which influences the size appearance of some controls.
29 | * @public
30 | * @returns css class, either 'sapUiSizeCompact' or 'sapUiSizeCozy' - or an empty string if no css class should be set
31 | */
32 | public getContentDensityClass(): string {
33 | if (this.contentDensityClass === undefined) {
34 | // check whether FLP has already set the content density class; do nothing in this case
35 | if (document.body.classList.contains("sapUiSizeCozy") || document.body.classList.contains("sapUiSizeCompact")) {
36 | this.contentDensityClass = "";
37 | } else if (!Device.support.touch) {
38 | // apply "compact" mode if touch is not supported
39 | this.contentDensityClass = "sapUiSizeCompact";
40 | } else {
41 | // "cozy" in case of touch support; default for most sap.m controls, but needed for desktop-first controls like sap.ui.table.Table
42 | this.contentDensityClass = "sapUiSizeCozy";
43 | }
44 | }
45 | return this.contentDensityClass;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/controller/App.controller.ts:
--------------------------------------------------------------------------------
1 | import BaseController from "./BaseController";
2 |
3 | /**
4 | * @namespace com.myorg.myapp.controller
5 | */
6 | export default class App extends BaseController {
7 | public onInit(): void {
8 | // apply content density mode to root view
9 | this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/controller/BaseController.ts:
--------------------------------------------------------------------------------
1 | import Controller from "sap/ui/core/mvc/Controller";
2 | import UIComponent from "sap/ui/core/UIComponent";
3 | import AppComponent from "../Component";
4 | import Model from "sap/ui/model/Model";
5 | import ResourceModel from "sap/ui/model/resource/ResourceModel";
6 | import ResourceBundle from "sap/base/i18n/ResourceBundle";
7 | import Router from "sap/ui/core/routing/Router";
8 | import History from "sap/ui/core/routing/History";
9 |
10 | /**
11 | * @namespace com.myorg.myapp.controller
12 | */
13 | export default abstract class BaseController extends Controller {
14 | /**
15 | * Convenience method for accessing the component of the controller's view.
16 | * @returns The component of the controller's view
17 | */
18 | public getOwnerComponent(): AppComponent {
19 | return super.getOwnerComponent() as AppComponent;
20 | }
21 |
22 | /**
23 | * Convenience method to get the components' router instance.
24 | * @returns The router instance
25 | */
26 | public getRouter(): Router {
27 | return UIComponent.getRouterFor(this);
28 | }
29 |
30 | /**
31 | * Convenience method for getting the i18n resource bundle of the component.
32 | * @returns The i18n resource bundle of the component
33 | */
34 | public getResourceBundle(): ResourceBundle | Promise {
35 | const oModel = this.getOwnerComponent().getModel("i18n") as ResourceModel;
36 | return oModel.getResourceBundle();
37 | }
38 |
39 | /**
40 | * Convenience method for getting the view model by name in every controller of the application.
41 | * @param [sName] The model name
42 | * @returns The model instance
43 | */
44 | public getModel(sName?: string): Model {
45 | return this.getView().getModel(sName);
46 | }
47 |
48 | /**
49 | * Convenience method for setting the view model in every controller of the application.
50 | * @param oModel The model instance
51 | * @param [sName] The model name
52 | * @returns The current base controller instance
53 | */
54 | public setModel(oModel: Model, sName?: string): BaseController {
55 | this.getView().setModel(oModel, sName);
56 | return this;
57 | }
58 |
59 | /**
60 | * Convenience method for triggering the navigation to a specific target.
61 | * @public
62 | * @param sName Target name
63 | * @param [oParameters] Navigation parameters
64 | * @param [bReplace] Defines if the hash should be replaced (no browser history entry) or set (browser history entry)
65 | */
66 | public navTo(sName: string, oParameters?: object, bReplace?: boolean): void {
67 | this.getRouter().navTo(sName, oParameters, undefined, bReplace);
68 | }
69 |
70 | /**
71 | * Convenience event handler for navigating back.
72 | * It there is a history entry we go one step back in the browser history
73 | * If not, it will replace the current entry of the browser history with the main route.
74 | */
75 | public onNavBack(): void {
76 | const sPreviousHash = History.getInstance().getPreviousHash();
77 | if (sPreviousHash !== undefined) {
78 | window.history.go(-1);
79 | } else {
80 | this.getRouter().navTo("main", {}, undefined, true);
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/controller/Main.controller.ts:
--------------------------------------------------------------------------------
1 | import MessageBox from "sap/m/MessageBox";
2 | import BaseController from "./BaseController";
3 |
4 | /**
5 | * @namespace com.myorg.myapp.controller
6 | */
7 | export default class Main extends BaseController {
8 | public sayHello(): void {
9 | MessageBox.show("Hello World!");
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/i18n/i18n.properties:
--------------------------------------------------------------------------------
1 | appTitle=com.myorg.myapp
2 | appDescription=UI5 Application com.myorg.myapp
3 | btnText=Say Hello
4 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/i18n/i18n_de.properties:
--------------------------------------------------------------------------------
1 | appTitle=com.myorg.myapp
2 | appDescription=UI5 Application com.myorg.myapp
3 | btnText=Sag Hallo
4 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/i18n/i18n_en.properties:
--------------------------------------------------------------------------------
1 | appTitle=com.myorg.myapp
2 | appDescription=UI5 Application com.myorg.myapp
3 | btnText=Say Hello
4 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/index-cdn.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | UI5 Application: com.myorg.myapp
11 |
12 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | UI5 Application: com.myorg.myapp
10 |
11 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "_version": "1.12.0",
3 |
4 | "sap.app": {
5 | "id": "com.myorg.myapp",
6 | "type": "application",
7 | "i18n": "i18n/i18n.properties",
8 | "title": "{{appTitle}}",
9 | "description": "{{appDescription}}",
10 | "applicationVersion": {
11 | "version": "1.0.0"
12 | }
13 | },
14 |
15 | "sap.ui": {
16 | "technology": "UI5",
17 | "icons": {},
18 | "deviceTypes": {
19 | "desktop": true,
20 | "tablet": true,
21 | "phone": true
22 | }
23 | },
24 |
25 | "sap.ui5": {
26 | "rootView": {
27 | "viewName": "com.myorg.myapp.view.App",
28 | "type": "XML",
29 | "async": true,
30 | "id": "app"
31 | },
32 |
33 | "dependencies": {
34 | "minUI5Version": "1.115.1",
35 | "libs": {
36 | "sap.ui.core": {},
37 | "sap.m": {}
38 | }
39 | },
40 |
41 | "handleValidation": true,
42 |
43 | "contentDensities": {
44 | "compact": true,
45 | "cozy": true
46 | },
47 |
48 | "models": {
49 | "i18n": {
50 | "type": "sap.ui.model.resource.ResourceModel",
51 | "settings": {
52 | "bundleName": "com.myorg.myapp.i18n.i18n"
53 | }
54 | }
55 | },
56 |
57 | "routing": {
58 | "config": {
59 | "routerClass": "sap.m.routing.Router",
60 | "viewType": "XML",
61 | "viewPath": "com.myorg.myapp.view",
62 | "controlId": "app",
63 | "controlAggregation": "pages",
64 | "async": true
65 | },
66 | "routes": [
67 | {
68 | "pattern": "",
69 | "name": "main",
70 | "target": "main"
71 | }
72 | ],
73 | "targets": {
74 | "main": {
75 | "viewId": "main",
76 | "viewName": "Main"
77 | }
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/model/formatter.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | formatValue: (value: string) => {
3 | return value?.toUpperCase();
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/model/models.ts:
--------------------------------------------------------------------------------
1 | import JSONModel from "sap/ui/model/json/JSONModel";
2 | import BindingMode from "sap/ui/model/BindingMode";
3 |
4 | import Device from "sap/ui/Device";
5 |
6 |
7 | export default {
8 | createDeviceModel: () => {
9 | const oModel = new JSONModel(Device);
10 | oModel.setDefaultBindingMode(BindingMode.OneWay);
11 | return oModel;
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/test/Test.qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/test/integration/HelloJourney.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-floating-promises */
2 | import opaTest from "sap/ui/test/opaQunit";
3 | import MainPage from "./pages/MainPage";
4 |
5 | const onTheMainPage = new MainPage();
6 |
7 | QUnit.module("Sample Hello Journey");
8 |
9 | opaTest("Should open the Hello dialog", function () {
10 | // Arrangements
11 | onTheMainPage.iStartMyUIComponent({
12 | componentConfig: {
13 | name: "com.myorg.myapp"
14 | }
15 | });
16 |
17 | // Actions
18 | onTheMainPage.iPressTheSayHelloWithDialogButton();
19 |
20 | // Assertions
21 | onTheMainPage.iShouldSeeTheHelloDialog();
22 |
23 | // Actions
24 | onTheMainPage.iPressTheOkButtonInTheDialog();
25 |
26 | // Assertions
27 | onTheMainPage.iShouldNotSeeTheHelloDialog();
28 |
29 | // Cleanup
30 | onTheMainPage.iTeardownMyApp();
31 | });
32 |
33 | opaTest("Should close the Hello dialog", function () {
34 | // Arrangements
35 | onTheMainPage.iStartMyUIComponent({
36 | componentConfig: {
37 | name: "com.myorg.myapp"
38 | }
39 | });
40 |
41 | // Actions
42 | onTheMainPage.iPressTheSayHelloWithDialogButton();
43 | onTheMainPage.iPressTheOkButtonInTheDialog();
44 |
45 | // Assertions
46 | onTheMainPage.iShouldNotSeeTheHelloDialog();
47 |
48 | // Cleanup
49 | onTheMainPage.iTeardownMyApp();
50 | });
51 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/test/integration/opaTests.qunit.ts:
--------------------------------------------------------------------------------
1 | import "./HelloJourney";
2 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/test/integration/pages/MainPage.ts:
--------------------------------------------------------------------------------
1 | import Opa5 from "sap/ui/test/Opa5";
2 | import Press from "sap/ui/test/actions/Press";
3 |
4 | const viewName = "com.myorg.myapp.view.Main";
5 |
6 | export default class MainPage extends Opa5 {
7 | // Actions
8 | iPressTheSayHelloWithDialogButton() {
9 | this.waitFor({
10 | id: "helloButton",
11 | viewName,
12 | actions: new Press(),
13 | errorMessage: "Did not find the 'Say Hello With Dialog' button on the Main view"
14 | });
15 | }
16 |
17 | iPressTheOkButtonInTheDialog() {
18 | this.waitFor({
19 | controlType: "sap.m.Button",
20 | searchOpenDialogs: true,
21 | viewName,
22 | actions: new Press(),
23 | errorMessage: "Did not find the 'OK' button in the Dialog"
24 | });
25 | }
26 |
27 | // Assertions
28 | iShouldSeeTheHelloDialog() {
29 | this.waitFor({
30 | controlType: "sap.m.Dialog",
31 | success: function () {
32 | // we set the view busy, so we need to query the parent of the app
33 | Opa5.assert.ok(true, "The dialog is open");
34 | },
35 | errorMessage: "Did not find the dialog control"
36 | });
37 | }
38 |
39 | iShouldNotSeeTheHelloDialog() {
40 | this.waitFor({
41 | controlType: "sap.m.App", // dummy, I just want a check function, where I can search the DOM. Probably there is a better way for a NEGATIVE test (NO dialog).
42 | check: function () {
43 | return document.querySelectorAll(".sapMDialog").length === 0;
44 | },
45 | success: function () {
46 | Opa5.assert.ok(true, "No dialog is open");
47 | }
48 | });
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/test/testsuite.qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/test/testsuite.qunit.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "Unit test suite for the UI5 Application: com.myorg.myapp",
3 | defaults: {
4 | page: "ui5://test-resources/com/myorg/myapp/Test.qunit.html?testsuite={suite}&test={name}",
5 | qunit: {
6 | version: 2
7 | },
8 | ui5: {
9 | theme: "sap_horizon"
10 | },
11 | loader: {
12 | paths: {
13 | "com/myorg/myapp": "../"
14 | }
15 | }
16 | },
17 | tests: {
18 | "unit/unitTests": {
19 | title: "Unit tests for the UI5 Application: com.myorg.myapp"
20 | },
21 | "integration/opaTests": {
22 | title: "Integration tests for the UI5 Application: com.myorg.myapp"
23 | }
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/test/unit/controller/Main.qunit.ts:
--------------------------------------------------------------------------------
1 | import Main from "com/myorg/myapp/controller/Main.controller";
2 |
3 | QUnit.module("Sample Main controller test");
4 |
5 | QUnit.test("The Main controller class has a sayHello method", function (assert) {
6 | // as a very basic test example just check the presence of the "sayHello" method
7 | assert.strictEqual(typeof Main.prototype.sayHello, "function");
8 | });
9 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/test/unit/unitTests.qunit.ts:
--------------------------------------------------------------------------------
1 | import "./controller/Main.qunit";
2 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/view/App.view.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/exercises/ex1/com.myorg.myapp/webapp/view/Main.view.xml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
17 |
18 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | # We recommend you to keep these unchanged
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 |
14 | # Change these settings to your own preference
15 | indent_style = tab
16 | indent_size = 2
17 |
18 | [*.{yaml,yml}]
19 | indent_style = space
20 |
21 | [*.md]
22 | indent_style = unset
23 | trim_trailing_whitespace = false
24 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "root": true,
3 | "env": {
4 | "browser": true,
5 | "es6": true,
6 | "node": true
7 | },
8 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking"],
9 | "parser": "@typescript-eslint/parser",
10 | "parserOptions": {
11 | "project": [`${__dirname}/tsconfig.json`],
12 | "sourceType": "module"
13 | },
14 | "plugins": ["@typescript-eslint"],
15 | "ignorePatterns": [".eslintrc.js"]
16 | };
17 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/.gitignore:
--------------------------------------------------------------------------------
1 | # build results
2 | dist
3 | coverage
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 |
12 | # Dependency directories
13 | node_modules/
14 |
15 | .DS_Store
16 | .env
17 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/.yo-rc.json:
--------------------------------------------------------------------------------
1 | {
2 | "generator-ui5-ts-app": {
3 | "namespace": "com.myorg.myapp",
4 | "framework": "OpenUI5",
5 | "frameworkVersion": "1.115.1",
6 | "author": "OpenUI5 Team",
7 | "initrepo": false,
8 | "tstypes": "@openui5/types",
9 | "tstypesVersion": "1.115.1",
10 | "appId": "com.myorg.myapp",
11 | "appURI": "com/myorg/myapp",
12 | "cdnDomain": "sdk.openui5.org",
13 | "defaultTheme": "sap_horizon",
14 | "gte11150": true,
15 | "setupCompleted": true
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/karma-ci-cov.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | require("./karma-ci.conf")(config);
3 | config.set({
4 | reporters: ["progress", "coverage"],
5 | preprocessors: {
6 | "webapp/**/*.ts": ["ui5-transpile"]
7 | },
8 | coverageReporter: {
9 | dir: "coverage",
10 | reporters: [
11 | { type: "html", subdir: "report-html" },
12 | { type: "cobertura", subdir: ".", file: "cobertura.txt" },
13 | { type: "lcovonly", subdir: ".", file: "report-lcovonly.txt" },
14 | { type: "text-summary" }
15 | ]
16 | }
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/karma-ci.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | require("./karma.conf")(config);
3 | config.set({
4 | browsers: ["ChromeHeadless"],
5 | singleRun: true
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/karma.conf.js:
--------------------------------------------------------------------------------
1 | // karma-ui5 usage: https://github.com/SAP/karma-ui5
2 | module.exports = function (config) {
3 | config.set({
4 | frameworks: ["ui5"],
5 | browsers: ["Chrome"]
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "com.myorg.myapp",
3 | "version": "1.0.0",
4 | "description": "UI5 Application: com.myorg.myapp",
5 | "author": "OpenUI5 Team",
6 | "license": "Apache-2.0",
7 | "scripts": {
8 | "clean": "rimraf dist coverage",
9 | "build": "ui5 build --clean-dest",
10 | "build:opt": "ui5 build self-contained --clean-dest --all",
11 | "start": "ui5 serve --port 8080 -o index.html",
12 | "start-cdn": "ui5 serve --port 8080 -o index-cdn.html",
13 | "start:dist": "npm start -- --config ui5-dist.yaml",
14 | "start:dist-cdn": "npm run start-cdn -- --config ui5-dist.yaml",
15 | "ts-typecheck": "tsc --noEmit",
16 | "lint": "eslint webapp",
17 | "karma": "karma start",
18 | "karma-ci": "karma start karma-ci.conf.js",
19 | "karma-ci-cov": "karma start karma-ci-cov.conf.js",
20 | "test": "npm run lint && npm run karma-ci-cov"
21 | },
22 | "devDependencies": {
23 | "@openui5/types": "1.131.1",
24 | "@typescript-eslint/eslint-plugin": "^5.61.0",
25 | "@typescript-eslint/parser": "^5.61.0",
26 | "@ui5/cli": "^4.0.11",
27 | "eslint": "^8.44.0",
28 | "karma": "^6.4.2",
29 | "karma-chrome-launcher": "^3.2.0",
30 | "karma-coverage": "^2.2.1",
31 | "karma-ui5": "^3.0.3",
32 | "karma-ui5-transpile": "^0.3.22",
33 | "rimraf": "^5.0.1",
34 | "typescript": "^5.1.6",
35 | "ui5-middleware-livereload": "^0.8.4",
36 | "ui5-tooling-transpile": "^0.7.17"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2022",
4 | "module": "es2022",
5 | "moduleResolution": "node",
6 | "skipLibCheck": true,
7 | "allowJs": true,
8 | "strict": true,
9 | "strictNullChecks": false,
10 | "strictPropertyInitialization": false,
11 | "rootDir": "./webapp",
12 | "types": ["@openui5/types", "@types/qunit"],
13 | "paths": {
14 | "com/myorg/myapp/*": ["./webapp/*"],
15 | "unit/*": ["./webapp/test/unit/*"],
16 | "integration/*": ["./webapp/test/integration/*"]
17 | }
18 | },
19 | "include": ["./webapp/**/*"],
20 | "exclude": ["./webapp/coverage/**/*"]
21 | }
22 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/ui5-dist.yaml:
--------------------------------------------------------------------------------
1 | specVersion: "3.0"
2 | metadata:
3 | name: com.myorg.myapp
4 | type: application
5 | resources:
6 | configuration:
7 | paths:
8 | webapp: dist
9 | framework:
10 | name: OpenUI5
11 | version: "1.131.1"
12 | libraries:
13 | - name: sap.m
14 | - name: sap.ui.core
15 | - name: sap.ui.layout
16 | - name: themelib_sap_horizon
17 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/ui5.yaml:
--------------------------------------------------------------------------------
1 | specVersion: "3.0"
2 | metadata:
3 | name: com.myorg.myapp
4 | type: application
5 | framework:
6 | name: OpenUI5
7 | version: "1.131.1"
8 | libraries:
9 | - name: sap.m
10 | - name: sap.ui.core
11 | - name: sap.ui.layout
12 | - name: themelib_sap_horizon
13 | builder:
14 | customTasks:
15 | - name: ui5-tooling-transpile-task
16 | afterTask: replaceVersion
17 | server:
18 | customMiddleware:
19 | - name: ui5-tooling-transpile-middleware
20 | afterMiddleware: compression
21 | - name: ui5-middleware-livereload
22 | afterMiddleware: compression
23 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/Component.ts:
--------------------------------------------------------------------------------
1 | import UIComponent from "sap/ui/core/UIComponent";
2 | import models from "./model/models";
3 | import Device from "sap/ui/Device";
4 |
5 | /**
6 | * @namespace com.myorg.myapp
7 | */
8 | export default class Component extends UIComponent {
9 | public static metadata = {
10 | manifest: "json",
11 | };
12 |
13 | private contentDensityClass: string;
14 |
15 | public init(): void {
16 | // call the base component's init function
17 | super.init();
18 |
19 | // create the device model
20 | this.setModel(models.createDeviceModel(), "device");
21 |
22 | // create the views based on the url/hash
23 | this.getRouter().initialize();
24 | }
25 |
26 | /**
27 | * This method can be called to determine whether the sapUiSizeCompact or sapUiSizeCozy
28 | * design mode class should be set, which influences the size appearance of some controls.
29 | * @public
30 | * @returns css class, either 'sapUiSizeCompact' or 'sapUiSizeCozy' - or an empty string if no css class should be set
31 | */
32 | public getContentDensityClass(): string {
33 | if (this.contentDensityClass === undefined) {
34 | // check whether FLP has already set the content density class; do nothing in this case
35 | if (document.body.classList.contains("sapUiSizeCozy") || document.body.classList.contains("sapUiSizeCompact")) {
36 | this.contentDensityClass = "";
37 | } else if (!Device.support.touch) {
38 | // apply "compact" mode if touch is not supported
39 | this.contentDensityClass = "sapUiSizeCompact";
40 | } else {
41 | // "cozy" in case of touch support; default for most sap.m controls, but needed for desktop-first controls like sap.ui.table.Table
42 | this.contentDensityClass = "sapUiSizeCozy";
43 | }
44 | }
45 | return this.contentDensityClass;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/controller/App.controller.ts:
--------------------------------------------------------------------------------
1 | import BaseController from "./BaseController";
2 |
3 | /**
4 | * @namespace com.myorg.myapp.controller
5 | */
6 | export default class App extends BaseController {
7 | public onInit(): void {
8 | // apply content density mode to root view
9 | this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/controller/BaseController.ts:
--------------------------------------------------------------------------------
1 | import Controller from "sap/ui/core/mvc/Controller";
2 | import UIComponent from "sap/ui/core/UIComponent";
3 | import AppComponent from "../Component";
4 | import Model from "sap/ui/model/Model";
5 | import ResourceModel from "sap/ui/model/resource/ResourceModel";
6 | import ResourceBundle from "sap/base/i18n/ResourceBundle";
7 | import Router from "sap/ui/core/routing/Router";
8 | import History from "sap/ui/core/routing/History";
9 |
10 | /**
11 | * @namespace com.myorg.myapp.controller
12 | */
13 | export default abstract class BaseController extends Controller {
14 | /**
15 | * Convenience method for accessing the component of the controller's view.
16 | * @returns The component of the controller's view
17 | */
18 | public getOwnerComponent(): AppComponent {
19 | return super.getOwnerComponent() as AppComponent;
20 | }
21 |
22 | /**
23 | * Convenience method to get the components' router instance.
24 | * @returns The router instance
25 | */
26 | public getRouter(): Router {
27 | return UIComponent.getRouterFor(this);
28 | }
29 |
30 | /**
31 | * Convenience method for getting the i18n resource bundle of the component.
32 | * @returns The i18n resource bundle of the component
33 | */
34 | public getResourceBundle(): ResourceBundle | Promise {
35 | const oModel = this.getOwnerComponent().getModel("i18n") as ResourceModel;
36 | return oModel.getResourceBundle();
37 | }
38 |
39 | /**
40 | * Convenience method for getting the view model by name in every controller of the application.
41 | * @param [sName] The model name
42 | * @returns The model instance
43 | */
44 | public getModel(sName?: string): Model {
45 | return this.getView().getModel(sName);
46 | }
47 |
48 | /**
49 | * Convenience method for setting the view model in every controller of the application.
50 | * @param oModel The model instance
51 | * @param [sName] The model name
52 | * @returns The current base controller instance
53 | */
54 | public setModel(oModel: Model, sName?: string): BaseController {
55 | this.getView().setModel(oModel, sName);
56 | return this;
57 | }
58 |
59 | /**
60 | * Convenience method for triggering the navigation to a specific target.
61 | * @public
62 | * @param sName Target name
63 | * @param [oParameters] Navigation parameters
64 | * @param [bReplace] Defines if the hash should be replaced (no browser history entry) or set (browser history entry)
65 | */
66 | public navTo(sName: string, oParameters?: object, bReplace?: boolean): void {
67 | this.getRouter().navTo(sName, oParameters, undefined, bReplace);
68 | }
69 |
70 | /**
71 | * Convenience event handler for navigating back.
72 | * It there is a history entry we go one step back in the browser history
73 | * If not, it will replace the current entry of the browser history with the main route.
74 | */
75 | public onNavBack(): void {
76 | const sPreviousHash = History.getInstance().getPreviousHash();
77 | if (sPreviousHash !== undefined) {
78 | window.history.go(-1);
79 | } else {
80 | this.getRouter().navTo("main", {}, undefined, true);
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/controller/Main.controller.ts:
--------------------------------------------------------------------------------
1 | import BaseController from "./BaseController";
2 | import JSONModel from "sap/ui/model/json/JSONModel";
3 |
4 | type WeatherInfo = {
5 | current_weather: {
6 | temperature: number,
7 | windspeed: number,
8 | winddirection: number
9 | }
10 | }
11 |
12 | /**
13 | * @namespace com.myorg.myapp.controller
14 | */
15 | export default class Main extends BaseController {
16 | onInit(): void {
17 | const model = new JSONModel();
18 | this.setModel(model);
19 | void this.loadWeatherData();
20 | }
21 |
22 | async loadWeatherData(lat = "49.31", lon = "8.64") { // default coordinates: Walldorf
23 | const response = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true`);
24 | const jsonData = await response.json() as WeatherInfo;
25 | (this.getModel() as JSONModel).setData(jsonData);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/i18n/i18n.properties:
--------------------------------------------------------------------------------
1 | appTitle=com.myorg.myapp
2 | appDescription=UI5 Application com.myorg.myapp
3 | btnText=Say Hello
4 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/i18n/i18n_de.properties:
--------------------------------------------------------------------------------
1 | appTitle=com.myorg.myapp
2 | appDescription=UI5 Application com.myorg.myapp
3 | btnText=Sag Hallo
4 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/i18n/i18n_en.properties:
--------------------------------------------------------------------------------
1 | appTitle=com.myorg.myapp
2 | appDescription=UI5 Application com.myorg.myapp
3 | btnText=Say Hello
4 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/index-cdn.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | UI5 Application: com.myorg.myapp
11 |
12 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | UI5 Application: com.myorg.myapp
10 |
11 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "_version": "1.12.0",
3 |
4 | "sap.app": {
5 | "id": "com.myorg.myapp",
6 | "type": "application",
7 | "i18n": "i18n/i18n.properties",
8 | "title": "{{appTitle}}",
9 | "description": "{{appDescription}}",
10 | "applicationVersion": {
11 | "version": "1.0.0"
12 | }
13 | },
14 |
15 | "sap.ui": {
16 | "technology": "UI5",
17 | "icons": {},
18 | "deviceTypes": {
19 | "desktop": true,
20 | "tablet": true,
21 | "phone": true
22 | }
23 | },
24 |
25 | "sap.ui5": {
26 | "rootView": {
27 | "viewName": "com.myorg.myapp.view.App",
28 | "type": "XML",
29 | "async": true,
30 | "id": "app"
31 | },
32 |
33 | "dependencies": {
34 | "minUI5Version": "1.115.1",
35 | "libs": {
36 | "sap.ui.core": {},
37 | "sap.m": {},
38 | "sap.ui.layout": {}
39 | }
40 | },
41 |
42 | "handleValidation": true,
43 |
44 | "contentDensities": {
45 | "compact": true,
46 | "cozy": true
47 | },
48 |
49 | "models": {
50 | "i18n": {
51 | "type": "sap.ui.model.resource.ResourceModel",
52 | "settings": {
53 | "bundleName": "com.myorg.myapp.i18n.i18n"
54 | }
55 | }
56 | },
57 |
58 | "routing": {
59 | "config": {
60 | "routerClass": "sap.m.routing.Router",
61 | "viewType": "XML",
62 | "viewPath": "com.myorg.myapp.view",
63 | "controlId": "app",
64 | "controlAggregation": "pages",
65 | "async": true
66 | },
67 | "routes": [
68 | {
69 | "pattern": "",
70 | "name": "main",
71 | "target": "main"
72 | }
73 | ],
74 | "targets": {
75 | "main": {
76 | "viewId": "main",
77 | "viewName": "Main"
78 | }
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/model/formatter.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | formatValue: (value: string) => {
3 | return value?.toUpperCase();
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/model/models.ts:
--------------------------------------------------------------------------------
1 | import JSONModel from "sap/ui/model/json/JSONModel";
2 | import BindingMode from "sap/ui/model/BindingMode";
3 |
4 | import Device from "sap/ui/Device";
5 |
6 |
7 | export default {
8 | createDeviceModel: () => {
9 | const oModel = new JSONModel(Device);
10 | oModel.setDefaultBindingMode(BindingMode.OneWay);
11 | return oModel;
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/test/Test.qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/test/integration/HelloJourney.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-floating-promises */
2 | import opaTest from "sap/ui/test/opaQunit";
3 | import MainPage from "./pages/MainPage";
4 |
5 | const onTheMainPage = new MainPage();
6 |
7 | QUnit.module("Sample Hello Journey");
8 |
9 | opaTest("Should open the Hello dialog", function () {
10 | // Arrangements
11 | onTheMainPage.iStartMyUIComponent({
12 | componentConfig: {
13 | name: "com.myorg.myapp"
14 | }
15 | });
16 |
17 | // Actions
18 | onTheMainPage.iPressTheSayHelloWithDialogButton();
19 |
20 | // Assertions
21 | onTheMainPage.iShouldSeeTheHelloDialog();
22 |
23 | // Actions
24 | onTheMainPage.iPressTheOkButtonInTheDialog();
25 |
26 | // Assertions
27 | onTheMainPage.iShouldNotSeeTheHelloDialog();
28 |
29 | // Cleanup
30 | onTheMainPage.iTeardownMyApp();
31 | });
32 |
33 | opaTest("Should close the Hello dialog", function () {
34 | // Arrangements
35 | onTheMainPage.iStartMyUIComponent({
36 | componentConfig: {
37 | name: "com.myorg.myapp"
38 | }
39 | });
40 |
41 | // Actions
42 | onTheMainPage.iPressTheSayHelloWithDialogButton();
43 | onTheMainPage.iPressTheOkButtonInTheDialog();
44 |
45 | // Assertions
46 | onTheMainPage.iShouldNotSeeTheHelloDialog();
47 |
48 | // Cleanup
49 | onTheMainPage.iTeardownMyApp();
50 | });
51 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/test/integration/opaTests.qunit.ts:
--------------------------------------------------------------------------------
1 | import "./HelloJourney";
2 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/test/integration/pages/MainPage.ts:
--------------------------------------------------------------------------------
1 | import Opa5 from "sap/ui/test/Opa5";
2 | import Press from "sap/ui/test/actions/Press";
3 |
4 | const viewName = "com.myorg.myapp.view.Main";
5 |
6 | export default class MainPage extends Opa5 {
7 | // Actions
8 | iPressTheSayHelloWithDialogButton() {
9 | this.waitFor({
10 | id: "helloButton",
11 | viewName,
12 | actions: new Press(),
13 | errorMessage: "Did not find the 'Say Hello With Dialog' button on the Main view"
14 | });
15 | }
16 |
17 | iPressTheOkButtonInTheDialog() {
18 | this.waitFor({
19 | controlType: "sap.m.Button",
20 | searchOpenDialogs: true,
21 | viewName,
22 | actions: new Press(),
23 | errorMessage: "Did not find the 'OK' button in the Dialog"
24 | });
25 | }
26 |
27 | // Assertions
28 | iShouldSeeTheHelloDialog() {
29 | this.waitFor({
30 | controlType: "sap.m.Dialog",
31 | success: function () {
32 | // we set the view busy, so we need to query the parent of the app
33 | Opa5.assert.ok(true, "The dialog is open");
34 | },
35 | errorMessage: "Did not find the dialog control"
36 | });
37 | }
38 |
39 | iShouldNotSeeTheHelloDialog() {
40 | this.waitFor({
41 | controlType: "sap.m.App", // dummy, I just want a check function, where I can search the DOM. Probably there is a better way for a NEGATIVE test (NO dialog).
42 | check: function () {
43 | return document.querySelectorAll(".sapMDialog").length === 0;
44 | },
45 | success: function () {
46 | Opa5.assert.ok(true, "No dialog is open");
47 | }
48 | });
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/test/testsuite.qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/test/testsuite.qunit.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "Unit test suite for the UI5 Application: com.myorg.myapp",
3 | defaults: {
4 | page: "ui5://test-resources/com/myorg/myapp/Test.qunit.html?testsuite={suite}&test={name}",
5 | qunit: {
6 | version: 2
7 | },
8 | ui5: {
9 | theme: "sap_horizon"
10 | },
11 | loader: {
12 | paths: {
13 | "com/myorg/myapp": "../"
14 | }
15 | }
16 | },
17 | tests: {
18 | "unit/unitTests": {
19 | title: "Unit tests for the UI5 Application: com.myorg.myapp"
20 | },
21 | "integration/opaTests": {
22 | title: "Integration tests for the UI5 Application: com.myorg.myapp"
23 | }
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/test/unit/controller/Main.qunit.ts:
--------------------------------------------------------------------------------
1 | import Main from "com/myorg/myapp/controller/Main.controller";
2 |
3 | QUnit.module("Sample Main controller test");
4 |
5 | QUnit.test("The Main controller class has a sayHello method", function (assert) {
6 | // as a very basic test example just check the presence of the "sayHello" method
7 | assert.strictEqual(typeof Main.prototype.sayHello, "function");
8 | });
9 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/test/unit/unitTests.qunit.ts:
--------------------------------------------------------------------------------
1 | import "./controller/Main.qunit";
2 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/view/App.view.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/exercises/ex2/com.myorg.myapp/webapp/view/Main.view.xml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/exercises/ex2/images/codeassist_jsonmodel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SAP-samples/ui5-typescript-tutorial/HEAD/exercises/ex2/images/codeassist_jsonmodel.png
--------------------------------------------------------------------------------
/exercises/ex2/images/create_onInit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SAP-samples/ui5-typescript-tutorial/HEAD/exercises/ex2/images/create_onInit.png
--------------------------------------------------------------------------------
/exercises/ex2/images/ex2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SAP-samples/ui5-typescript-tutorial/HEAD/exercises/ex2/images/ex2.png
--------------------------------------------------------------------------------
/exercises/ex2/images/hover_init.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SAP-samples/ui5-typescript-tutorial/HEAD/exercises/ex2/images/hover_init.png
--------------------------------------------------------------------------------
/exercises/ex2/images/jsonmodel_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SAP-samples/ui5-typescript-tutorial/HEAD/exercises/ex2/images/jsonmodel_error.png
--------------------------------------------------------------------------------
/exercises/ex2/images/jsonmodel_quickfix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SAP-samples/ui5-typescript-tutorial/HEAD/exercises/ex2/images/jsonmodel_quickfix.png
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | # We recommend you to keep these unchanged
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 |
14 | # Change these settings to your own preference
15 | indent_style = tab
16 | indent_size = 2
17 |
18 | [*.{yaml,yml}]
19 | indent_style = space
20 |
21 | [*.md]
22 | indent_style = unset
23 | trim_trailing_whitespace = false
24 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "root": true,
3 | "env": {
4 | "browser": true,
5 | "es6": true,
6 | "node": true
7 | },
8 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking"],
9 | "parser": "@typescript-eslint/parser",
10 | "parserOptions": {
11 | "project": [`${__dirname}/tsconfig.json`],
12 | "sourceType": "module"
13 | },
14 | "plugins": ["@typescript-eslint"],
15 | "ignorePatterns": [".eslintrc.js"]
16 | };
17 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/.gitignore:
--------------------------------------------------------------------------------
1 | # build results
2 | dist
3 | coverage
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 |
12 | # Dependency directories
13 | node_modules/
14 |
15 | .DS_Store
16 | .env
17 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/.yo-rc.json:
--------------------------------------------------------------------------------
1 | {
2 | "generator-ui5-ts-app": {
3 | "namespace": "com.myorg.myapp",
4 | "framework": "OpenUI5",
5 | "frameworkVersion": "1.115.1",
6 | "author": "OpenUI5 Team",
7 | "initrepo": false,
8 | "tstypes": "@openui5/types",
9 | "tstypesVersion": "1.115.1",
10 | "appId": "com.myorg.myapp",
11 | "appURI": "com/myorg/myapp",
12 | "cdnDomain": "sdk.openui5.org",
13 | "defaultTheme": "sap_horizon",
14 | "gte11150": true,
15 | "setupCompleted": true
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/karma-ci-cov.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | require("./karma-ci.conf")(config);
3 | config.set({
4 | reporters: ["progress", "coverage"],
5 | preprocessors: {
6 | "webapp/**/*.ts": ["ui5-transpile"]
7 | },
8 | coverageReporter: {
9 | dir: "coverage",
10 | reporters: [
11 | { type: "html", subdir: "report-html" },
12 | { type: "cobertura", subdir: ".", file: "cobertura.txt" },
13 | { type: "lcovonly", subdir: ".", file: "report-lcovonly.txt" },
14 | { type: "text-summary" }
15 | ]
16 | }
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/karma-ci.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | require("./karma.conf")(config);
3 | config.set({
4 | browsers: ["ChromeHeadless"],
5 | singleRun: true
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/karma.conf.js:
--------------------------------------------------------------------------------
1 | // karma-ui5 usage: https://github.com/SAP/karma-ui5
2 | module.exports = function (config) {
3 | config.set({
4 | frameworks: ["ui5"],
5 | browsers: ["Chrome"]
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "com.myorg.myapp",
3 | "version": "1.0.0",
4 | "description": "UI5 Application: com.myorg.myapp",
5 | "author": "OpenUI5 Team",
6 | "license": "Apache-2.0",
7 | "scripts": {
8 | "clean": "rimraf dist coverage",
9 | "build": "ui5 build --clean-dest",
10 | "build:opt": "ui5 build self-contained --clean-dest --all",
11 | "start": "ui5 serve --port 8080 -o index.html",
12 | "start-cdn": "ui5 serve --port 8080 -o index-cdn.html",
13 | "start:dist": "npm start -- --config ui5-dist.yaml",
14 | "start:dist-cdn": "npm run start-cdn -- --config ui5-dist.yaml",
15 | "ts-typecheck": "tsc --noEmit",
16 | "lint": "eslint webapp",
17 | "karma": "karma start",
18 | "karma-ci": "karma start karma-ci.conf.js",
19 | "karma-ci-cov": "karma start karma-ci-cov.conf.js",
20 | "test": "npm run lint && npm run karma-ci-cov"
21 | },
22 | "devDependencies": {
23 | "@openui5/types": "1.115.1",
24 | "@typescript-eslint/eslint-plugin": "^5.61.0",
25 | "@typescript-eslint/parser": "^5.61.0",
26 | "@ui5/cli": "^4.0.11",
27 | "eslint": "^8.44.0",
28 | "karma": "^6.4.2",
29 | "karma-chrome-launcher": "^3.2.0",
30 | "karma-coverage": "^2.2.1",
31 | "karma-ui5": "^3.0.3",
32 | "karma-ui5-transpile": "^0.3.22",
33 | "rimraf": "^5.0.1",
34 | "typescript": "^5.1.6",
35 | "ui5-middleware-livereload": "^0.8.4",
36 | "ui5-tooling-transpile": "^0.7.17"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2022",
4 | "module": "es2022",
5 | "moduleResolution": "node",
6 | "skipLibCheck": true,
7 | "allowJs": true,
8 | "strict": true,
9 | "strictNullChecks": false,
10 | "strictPropertyInitialization": false,
11 | "rootDir": "./webapp",
12 | "types": ["@openui5/types", "@types/qunit"],
13 | "paths": {
14 | "com/myorg/myapp/*": ["./webapp/*"],
15 | "unit/*": ["./webapp/test/unit/*"],
16 | "integration/*": ["./webapp/test/integration/*"]
17 | }
18 | },
19 | "include": ["./webapp/**/*"],
20 | "exclude": ["./webapp/coverage/**/*"]
21 | }
22 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/ui5-dist.yaml:
--------------------------------------------------------------------------------
1 | specVersion: "3.0"
2 | metadata:
3 | name: com.myorg.myapp
4 | type: application
5 | resources:
6 | configuration:
7 | paths:
8 | webapp: dist
9 | framework:
10 | name: OpenUI5
11 | version: "1.131.1"
12 | libraries:
13 | - name: sap.m
14 | - name: sap.ui.core
15 | - name: sap.ui.layout
16 | - name: themelib_sap_horizon
17 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/ui5.yaml:
--------------------------------------------------------------------------------
1 | specVersion: "3.0"
2 | metadata:
3 | name: com.myorg.myapp
4 | type: application
5 | framework:
6 | name: OpenUI5
7 | version: "1.131.1"
8 | libraries:
9 | - name: sap.m
10 | - name: sap.ui.core
11 | - name: sap.ui.layout
12 | - name: themelib_sap_horizon
13 | builder:
14 | customTasks:
15 | - name: ui5-tooling-transpile-task
16 | afterTask: replaceVersion
17 | server:
18 | customMiddleware:
19 | - name: ui5-tooling-transpile-middleware
20 | afterMiddleware: compression
21 | - name: ui5-middleware-livereload
22 | afterMiddleware: compression
23 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/Component.ts:
--------------------------------------------------------------------------------
1 | import UIComponent from "sap/ui/core/UIComponent";
2 | import models from "./model/models";
3 | import Device from "sap/ui/Device";
4 |
5 | /**
6 | * @namespace com.myorg.myapp
7 | */
8 | export default class Component extends UIComponent {
9 | public static metadata = {
10 | manifest: "json",
11 | };
12 |
13 | private contentDensityClass: string;
14 |
15 | public init(): void {
16 | // call the base component's init function
17 | super.init();
18 |
19 | // create the device model
20 | this.setModel(models.createDeviceModel(), "device");
21 |
22 | // create the views based on the url/hash
23 | this.getRouter().initialize();
24 | }
25 |
26 | /**
27 | * This method can be called to determine whether the sapUiSizeCompact or sapUiSizeCozy
28 | * design mode class should be set, which influences the size appearance of some controls.
29 | * @public
30 | * @returns css class, either 'sapUiSizeCompact' or 'sapUiSizeCozy' - or an empty string if no css class should be set
31 | */
32 | public getContentDensityClass(): string {
33 | if (this.contentDensityClass === undefined) {
34 | // check whether FLP has already set the content density class; do nothing in this case
35 | if (document.body.classList.contains("sapUiSizeCozy") || document.body.classList.contains("sapUiSizeCompact")) {
36 | this.contentDensityClass = "";
37 | } else if (!Device.support.touch) {
38 | // apply "compact" mode if touch is not supported
39 | this.contentDensityClass = "sapUiSizeCompact";
40 | } else {
41 | // "cozy" in case of touch support; default for most sap.m controls, but needed for desktop-first controls like sap.ui.table.Table
42 | this.contentDensityClass = "sapUiSizeCozy";
43 | }
44 | }
45 | return this.contentDensityClass;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/controller/App.controller.ts:
--------------------------------------------------------------------------------
1 | import BaseController from "./BaseController";
2 |
3 | /**
4 | * @namespace com.myorg.myapp.controller
5 | */
6 | export default class App extends BaseController {
7 | public onInit(): void {
8 | // apply content density mode to root view
9 | this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/controller/BaseController.ts:
--------------------------------------------------------------------------------
1 | import Controller from "sap/ui/core/mvc/Controller";
2 | import UIComponent from "sap/ui/core/UIComponent";
3 | import AppComponent from "../Component";
4 | import Model from "sap/ui/model/Model";
5 | import ResourceModel from "sap/ui/model/resource/ResourceModel";
6 | import ResourceBundle from "sap/base/i18n/ResourceBundle";
7 | import Router from "sap/ui/core/routing/Router";
8 | import History from "sap/ui/core/routing/History";
9 |
10 | /**
11 | * @namespace com.myorg.myapp.controller
12 | */
13 | export default abstract class BaseController extends Controller {
14 | /**
15 | * Convenience method for accessing the component of the controller's view.
16 | * @returns The component of the controller's view
17 | */
18 | public getOwnerComponent(): AppComponent {
19 | return super.getOwnerComponent() as AppComponent;
20 | }
21 |
22 | /**
23 | * Convenience method to get the components' router instance.
24 | * @returns The router instance
25 | */
26 | public getRouter(): Router {
27 | return UIComponent.getRouterFor(this);
28 | }
29 |
30 | /**
31 | * Convenience method for getting the i18n resource bundle of the component.
32 | * @returns The i18n resource bundle of the component
33 | */
34 | public getResourceBundle(): ResourceBundle | Promise {
35 | const oModel = this.getOwnerComponent().getModel("i18n") as ResourceModel;
36 | return oModel.getResourceBundle();
37 | }
38 |
39 | /**
40 | * Convenience method for getting the view model by name in every controller of the application.
41 | * @param [sName] The model name
42 | * @returns The model instance
43 | */
44 | public getModel(sName?: string): Model {
45 | return this.getView().getModel(sName);
46 | }
47 |
48 | /**
49 | * Convenience method for setting the view model in every controller of the application.
50 | * @param oModel The model instance
51 | * @param [sName] The model name
52 | * @returns The current base controller instance
53 | */
54 | public setModel(oModel: Model, sName?: string): BaseController {
55 | this.getView().setModel(oModel, sName);
56 | return this;
57 | }
58 |
59 | /**
60 | * Convenience method for triggering the navigation to a specific target.
61 | * @public
62 | * @param sName Target name
63 | * @param [oParameters] Navigation parameters
64 | * @param [bReplace] Defines if the hash should be replaced (no browser history entry) or set (browser history entry)
65 | */
66 | public navTo(sName: string, oParameters?: object, bReplace?: boolean): void {
67 | this.getRouter().navTo(sName, oParameters, undefined, bReplace);
68 | }
69 |
70 | /**
71 | * Convenience event handler for navigating back.
72 | * It there is a history entry we go one step back in the browser history
73 | * If not, it will replace the current entry of the browser history with the main route.
74 | */
75 | public onNavBack(): void {
76 | const sPreviousHash = History.getInstance().getPreviousHash();
77 | if (sPreviousHash !== undefined) {
78 | window.history.go(-1);
79 | } else {
80 | this.getRouter().navTo("main", {}, undefined, true);
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/controller/Main.controller.ts:
--------------------------------------------------------------------------------
1 | import { InputBase$ChangeEvent } from "sap/m/InputBase";
2 | import BaseController from "./BaseController";
3 | import JSONModel from "sap/ui/model/json/JSONModel";
4 |
5 | type WeatherInfo = {
6 | current_weather: {
7 | temperature: number,
8 | windspeed: number,
9 | winddirection: number
10 | }
11 | }
12 |
13 | /**
14 | * @namespace com.myorg.myapp.controller
15 | */
16 | export default class Main extends BaseController {
17 | onInit(): void {
18 | const model = new JSONModel();
19 | this.setModel(model);
20 | void this.loadWeatherData();
21 |
22 | /*
23 | // ALTERNATIVE to declarative event handler attaching in XMLView
24 | const input = this.byId("location");
25 | if (input.isA("sap.m.Input")) { // type guard (unfortunately the control class needs to be given twice)
26 | input.attachChange(function(evt) { // now TS knows input is an Input
27 | const location = evt.getParameter("value"); // type safety even for string-based access
28 | });
29 | }
30 | */
31 | }
32 |
33 | async loadWeatherData(lat = "49.31", lon = "8.64") { // default coordinates: Walldorf
34 | const response = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true`);
35 | const jsonData = await response.json() as WeatherInfo;
36 | (this.getModel() as JSONModel).setData(jsonData);
37 | }
38 |
39 | locationChange(evt: InputBase$ChangeEvent) {
40 | const location = evt.getParameters().value;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/i18n/i18n.properties:
--------------------------------------------------------------------------------
1 | appTitle=com.myorg.myapp
2 | appDescription=UI5 Application com.myorg.myapp
3 | btnText=Say Hello
4 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/i18n/i18n_de.properties:
--------------------------------------------------------------------------------
1 | appTitle=com.myorg.myapp
2 | appDescription=UI5 Application com.myorg.myapp
3 | btnText=Sag Hallo
4 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/i18n/i18n_en.properties:
--------------------------------------------------------------------------------
1 | appTitle=com.myorg.myapp
2 | appDescription=UI5 Application com.myorg.myapp
3 | btnText=Say Hello
4 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/index-cdn.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | UI5 Application: com.myorg.myapp
11 |
12 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | UI5 Application: com.myorg.myapp
10 |
11 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "_version": "1.12.0",
3 |
4 | "sap.app": {
5 | "id": "com.myorg.myapp",
6 | "type": "application",
7 | "i18n": "i18n/i18n.properties",
8 | "title": "{{appTitle}}",
9 | "description": "{{appDescription}}",
10 | "applicationVersion": {
11 | "version": "1.0.0"
12 | }
13 | },
14 |
15 | "sap.ui": {
16 | "technology": "UI5",
17 | "icons": {},
18 | "deviceTypes": {
19 | "desktop": true,
20 | "tablet": true,
21 | "phone": true
22 | }
23 | },
24 |
25 | "sap.ui5": {
26 | "rootView": {
27 | "viewName": "com.myorg.myapp.view.App",
28 | "type": "XML",
29 | "async": true,
30 | "id": "app"
31 | },
32 |
33 | "dependencies": {
34 | "minUI5Version": "1.115.1",
35 | "libs": {
36 | "sap.ui.core": {},
37 | "sap.m": {},
38 | "sap.ui.layout": {}
39 | }
40 | },
41 |
42 | "handleValidation": true,
43 |
44 | "contentDensities": {
45 | "compact": true,
46 | "cozy": true
47 | },
48 |
49 | "models": {
50 | "i18n": {
51 | "type": "sap.ui.model.resource.ResourceModel",
52 | "settings": {
53 | "bundleName": "com.myorg.myapp.i18n.i18n"
54 | }
55 | }
56 | },
57 |
58 | "routing": {
59 | "config": {
60 | "routerClass": "sap.m.routing.Router",
61 | "viewType": "XML",
62 | "viewPath": "com.myorg.myapp.view",
63 | "controlId": "app",
64 | "controlAggregation": "pages",
65 | "async": true
66 | },
67 | "routes": [
68 | {
69 | "pattern": "",
70 | "name": "main",
71 | "target": "main"
72 | }
73 | ],
74 | "targets": {
75 | "main": {
76 | "viewId": "main",
77 | "viewName": "Main"
78 | }
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/model/formatter.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | formatValue: (value: string) => {
3 | return value?.toUpperCase();
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/model/models.ts:
--------------------------------------------------------------------------------
1 | import JSONModel from "sap/ui/model/json/JSONModel";
2 | import BindingMode from "sap/ui/model/BindingMode";
3 |
4 | import Device from "sap/ui/Device";
5 |
6 |
7 | export default {
8 | createDeviceModel: () => {
9 | const oModel = new JSONModel(Device);
10 | oModel.setDefaultBindingMode(BindingMode.OneWay);
11 | return oModel;
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/test/Test.qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/test/integration/HelloJourney.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-floating-promises */
2 | import opaTest from "sap/ui/test/opaQunit";
3 | import MainPage from "./pages/MainPage";
4 |
5 | const onTheMainPage = new MainPage();
6 |
7 | QUnit.module("Sample Hello Journey");
8 |
9 | opaTest("Should open the Hello dialog", function () {
10 | // Arrangements
11 | onTheMainPage.iStartMyUIComponent({
12 | componentConfig: {
13 | name: "com.myorg.myapp"
14 | }
15 | });
16 |
17 | // Actions
18 | onTheMainPage.iPressTheSayHelloWithDialogButton();
19 |
20 | // Assertions
21 | onTheMainPage.iShouldSeeTheHelloDialog();
22 |
23 | // Actions
24 | onTheMainPage.iPressTheOkButtonInTheDialog();
25 |
26 | // Assertions
27 | onTheMainPage.iShouldNotSeeTheHelloDialog();
28 |
29 | // Cleanup
30 | onTheMainPage.iTeardownMyApp();
31 | });
32 |
33 | opaTest("Should close the Hello dialog", function () {
34 | // Arrangements
35 | onTheMainPage.iStartMyUIComponent({
36 | componentConfig: {
37 | name: "com.myorg.myapp"
38 | }
39 | });
40 |
41 | // Actions
42 | onTheMainPage.iPressTheSayHelloWithDialogButton();
43 | onTheMainPage.iPressTheOkButtonInTheDialog();
44 |
45 | // Assertions
46 | onTheMainPage.iShouldNotSeeTheHelloDialog();
47 |
48 | // Cleanup
49 | onTheMainPage.iTeardownMyApp();
50 | });
51 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/test/integration/opaTests.qunit.ts:
--------------------------------------------------------------------------------
1 | import "./HelloJourney";
2 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/test/integration/pages/MainPage.ts:
--------------------------------------------------------------------------------
1 | import Opa5 from "sap/ui/test/Opa5";
2 | import Press from "sap/ui/test/actions/Press";
3 |
4 | const viewName = "com.myorg.myapp.view.Main";
5 |
6 | export default class MainPage extends Opa5 {
7 | // Actions
8 | iPressTheSayHelloWithDialogButton() {
9 | this.waitFor({
10 | id: "helloButton",
11 | viewName,
12 | actions: new Press(),
13 | errorMessage: "Did not find the 'Say Hello With Dialog' button on the Main view"
14 | });
15 | }
16 |
17 | iPressTheOkButtonInTheDialog() {
18 | this.waitFor({
19 | controlType: "sap.m.Button",
20 | searchOpenDialogs: true,
21 | viewName,
22 | actions: new Press(),
23 | errorMessage: "Did not find the 'OK' button in the Dialog"
24 | });
25 | }
26 |
27 | // Assertions
28 | iShouldSeeTheHelloDialog() {
29 | this.waitFor({
30 | controlType: "sap.m.Dialog",
31 | success: function () {
32 | // we set the view busy, so we need to query the parent of the app
33 | Opa5.assert.ok(true, "The dialog is open");
34 | },
35 | errorMessage: "Did not find the dialog control"
36 | });
37 | }
38 |
39 | iShouldNotSeeTheHelloDialog() {
40 | this.waitFor({
41 | controlType: "sap.m.App", // dummy, I just want a check function, where I can search the DOM. Probably there is a better way for a NEGATIVE test (NO dialog).
42 | check: function () {
43 | return document.querySelectorAll(".sapMDialog").length === 0;
44 | },
45 | success: function () {
46 | Opa5.assert.ok(true, "No dialog is open");
47 | }
48 | });
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/test/testsuite.qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/test/testsuite.qunit.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "Unit test suite for the UI5 Application: com.myorg.myapp",
3 | defaults: {
4 | page: "ui5://test-resources/com/myorg/myapp/Test.qunit.html?testsuite={suite}&test={name}",
5 | qunit: {
6 | version: 2
7 | },
8 | ui5: {
9 | theme: "sap_horizon"
10 | },
11 | loader: {
12 | paths: {
13 | "com/myorg/myapp": "../"
14 | }
15 | }
16 | },
17 | tests: {
18 | "unit/unitTests": {
19 | title: "Unit tests for the UI5 Application: com.myorg.myapp"
20 | },
21 | "integration/opaTests": {
22 | title: "Integration tests for the UI5 Application: com.myorg.myapp"
23 | }
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/test/unit/controller/Main.qunit.ts:
--------------------------------------------------------------------------------
1 | import Main from "com/myorg/myapp/controller/Main.controller";
2 |
3 | QUnit.module("Sample Main controller test");
4 |
5 | QUnit.test("The Main controller class has a sayHello method", function (assert) {
6 | // as a very basic test example just check the presence of the "sayHello" method
7 | assert.strictEqual(typeof Main.prototype.sayHello, "function");
8 | });
9 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/test/unit/unitTests.qunit.ts:
--------------------------------------------------------------------------------
1 | import "./controller/Main.qunit";
2 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/view/App.view.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/exercises/ex3/com.myorg.myapp/webapp/view/Main.view.xml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/exercises/ex3/images/event_parameter_any_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SAP-samples/ui5-typescript-tutorial/HEAD/exercises/ex3/images/event_parameter_any_error.png
--------------------------------------------------------------------------------
/exercises/ex3/images/event_parameters_content_assist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SAP-samples/ui5-typescript-tutorial/HEAD/exercises/ex3/images/event_parameters_content_assist.png
--------------------------------------------------------------------------------
/exercises/ex3/images/ex3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SAP-samples/ui5-typescript-tutorial/HEAD/exercises/ex3/images/ex3.png
--------------------------------------------------------------------------------
/exercises/ex3/images/wrong_event.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SAP-samples/ui5-typescript-tutorial/HEAD/exercises/ex3/images/wrong_event.png
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | # We recommend you to keep these unchanged
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 |
14 | # Change these settings to your own preference
15 | indent_style = tab
16 | indent_size = 2
17 |
18 | [*.{yaml,yml}]
19 | indent_style = space
20 |
21 | [*.md]
22 | indent_style = unset
23 | trim_trailing_whitespace = false
24 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "root": true,
3 | "env": {
4 | "browser": true,
5 | "es6": true,
6 | "node": true
7 | },
8 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking"],
9 | "parser": "@typescript-eslint/parser",
10 | "parserOptions": {
11 | "project": [`${__dirname}/tsconfig.json`],
12 | "sourceType": "module"
13 | },
14 | "plugins": ["@typescript-eslint"],
15 | "ignorePatterns": [".eslintrc.js"]
16 | };
17 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/.gitignore:
--------------------------------------------------------------------------------
1 | # build results
2 | dist
3 | coverage
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 |
12 | # Dependency directories
13 | node_modules/
14 |
15 | .DS_Store
16 | .env
17 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/.yo-rc.json:
--------------------------------------------------------------------------------
1 | {
2 | "generator-ui5-ts-app": {
3 | "namespace": "com.myorg.myapp",
4 | "framework": "OpenUI5",
5 | "frameworkVersion": "1.115.1",
6 | "author": "OpenUI5 Team",
7 | "initrepo": false,
8 | "tstypes": "@openui5/types",
9 | "tstypesVersion": "1.115.1",
10 | "appId": "com.myorg.myapp",
11 | "appURI": "com/myorg/myapp",
12 | "cdnDomain": "sdk.openui5.org",
13 | "defaultTheme": "sap_horizon",
14 | "gte11150": true,
15 | "setupCompleted": true
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/karma-ci-cov.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | require("./karma-ci.conf")(config);
3 | config.set({
4 | reporters: ["progress", "coverage"],
5 | preprocessors: {
6 | "webapp/**/*.ts": ["ui5-transpile"]
7 | },
8 | coverageReporter: {
9 | dir: "coverage",
10 | reporters: [
11 | { type: "html", subdir: "report-html" },
12 | { type: "cobertura", subdir: ".", file: "cobertura.txt" },
13 | { type: "lcovonly", subdir: ".", file: "report-lcovonly.txt" },
14 | { type: "text-summary" }
15 | ]
16 | }
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/karma-ci.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | require("./karma.conf")(config);
3 | config.set({
4 | browsers: ["ChromeHeadless"],
5 | singleRun: true
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/karma.conf.js:
--------------------------------------------------------------------------------
1 | // karma-ui5 usage: https://github.com/SAP/karma-ui5
2 | module.exports = function (config) {
3 | config.set({
4 | frameworks: ["ui5"],
5 | browsers: ["Chrome"]
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "com.myorg.myapp",
3 | "version": "1.0.0",
4 | "description": "UI5 Application: com.myorg.myapp",
5 | "author": "OpenUI5 Team",
6 | "license": "Apache-2.0",
7 | "scripts": {
8 | "clean": "rimraf dist coverage",
9 | "build": "ui5 build --clean-dest",
10 | "build:opt": "ui5 build self-contained --clean-dest --all",
11 | "start": "ui5 serve --port 8080 -o index.html",
12 | "start-cdn": "ui5 serve --port 8080 -o index-cdn.html",
13 | "start:dist": "npm start -- --config ui5-dist.yaml",
14 | "start:dist-cdn": "npm run start-cdn -- --config ui5-dist.yaml",
15 | "ts-typecheck": "tsc --noEmit",
16 | "lint": "eslint webapp",
17 | "karma": "karma start",
18 | "karma-ci": "karma start karma-ci.conf.js",
19 | "karma-ci-cov": "karma start karma-ci-cov.conf.js",
20 | "test": "npm run lint && npm run karma-ci-cov"
21 | },
22 | "devDependencies": {
23 | "@openui5/types": "1.131.1",
24 | "@typescript-eslint/eslint-plugin": "^5.61.0",
25 | "@typescript-eslint/parser": "^5.61.0",
26 | "@ui5/cli": "^4.0.11",
27 | "eslint": "^8.44.0",
28 | "karma": "^6.4.2",
29 | "karma-chrome-launcher": "^3.2.0",
30 | "karma-coverage": "^2.2.1",
31 | "karma-ui5": "^3.0.3",
32 | "karma-ui5-transpile": "^0.3.22",
33 | "nominatim-client": "^3.2.1",
34 | "rimraf": "^5.0.1",
35 | "typescript": "^5.1.6",
36 | "ui5-middleware-livereload": "^0.8.4",
37 | "ui5-tooling-modules": "^0.9.12",
38 | "ui5-tooling-transpile": "^0.7.17"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2022",
4 | "module": "es2022",
5 | "moduleResolution": "node",
6 | "skipLibCheck": true,
7 | "allowJs": true,
8 | "strict": true,
9 | "strictNullChecks": false,
10 | "strictPropertyInitialization": false,
11 | "rootDir": "./webapp",
12 | "types": ["@openui5/types", "@types/qunit"],
13 | "paths": {
14 | "com/myorg/myapp/*": ["./webapp/*"],
15 | "unit/*": ["./webapp/test/unit/*"],
16 | "integration/*": ["./webapp/test/integration/*"]
17 | }
18 | },
19 | "include": ["./webapp/**/*"],
20 | "exclude": ["./webapp/coverage/**/*"]
21 | }
22 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/ui5-dist.yaml:
--------------------------------------------------------------------------------
1 | specVersion: "3.0"
2 | metadata:
3 | name: com.myorg.myapp
4 | type: application
5 | resources:
6 | configuration:
7 | paths:
8 | webapp: dist
9 | framework:
10 | name: OpenUI5
11 | version: "1.131.1"
12 | libraries:
13 | - name: sap.m
14 | - name: sap.ui.core
15 | - name: sap.ui.layout
16 | - name: themelib_sap_horizon
17 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/ui5.yaml:
--------------------------------------------------------------------------------
1 | specVersion: "3.0"
2 | metadata:
3 | name: com.myorg.myapp
4 | type: application
5 | framework:
6 | name: OpenUI5
7 | version: "1.131.1"
8 | libraries:
9 | - name: sap.m
10 | - name: sap.ui.core
11 | - name: sap.ui.layout
12 | - name: themelib_sap_horizon
13 | builder:
14 | customTasks:
15 | - name: ui5-tooling-modules-task
16 | afterTask: replaceVersion
17 | configuration:
18 | addToNamespace: true
19 | - name: ui5-tooling-transpile-task
20 | afterTask: replaceVersion
21 | server:
22 | customMiddleware:
23 | - name: ui5-tooling-modules-middleware
24 | afterMiddleware: compression
25 | - name: ui5-tooling-transpile-middleware
26 | afterMiddleware: compression
27 | - name: ui5-middleware-livereload
28 | afterMiddleware: compression
29 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/Component.ts:
--------------------------------------------------------------------------------
1 | import UIComponent from "sap/ui/core/UIComponent";
2 | import models from "./model/models";
3 | import Device from "sap/ui/Device";
4 |
5 | /**
6 | * @namespace com.myorg.myapp
7 | */
8 | export default class Component extends UIComponent {
9 | public static metadata = {
10 | manifest: "json",
11 | };
12 |
13 | private contentDensityClass: string;
14 |
15 | public init(): void {
16 | // call the base component's init function
17 | super.init();
18 |
19 | // create the device model
20 | this.setModel(models.createDeviceModel(), "device");
21 |
22 | // create the views based on the url/hash
23 | this.getRouter().initialize();
24 | }
25 |
26 | /**
27 | * This method can be called to determine whether the sapUiSizeCompact or sapUiSizeCozy
28 | * design mode class should be set, which influences the size appearance of some controls.
29 | * @public
30 | * @returns css class, either 'sapUiSizeCompact' or 'sapUiSizeCozy' - or an empty string if no css class should be set
31 | */
32 | public getContentDensityClass(): string {
33 | if (this.contentDensityClass === undefined) {
34 | // check whether FLP has already set the content density class; do nothing in this case
35 | if (document.body.classList.contains("sapUiSizeCozy") || document.body.classList.contains("sapUiSizeCompact")) {
36 | this.contentDensityClass = "";
37 | } else if (!Device.support.touch) {
38 | // apply "compact" mode if touch is not supported
39 | this.contentDensityClass = "sapUiSizeCompact";
40 | } else {
41 | // "cozy" in case of touch support; default for most sap.m controls, but needed for desktop-first controls like sap.ui.table.Table
42 | this.contentDensityClass = "sapUiSizeCozy";
43 | }
44 | }
45 | return this.contentDensityClass;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/controller/App.controller.ts:
--------------------------------------------------------------------------------
1 | import BaseController from "./BaseController";
2 |
3 | /**
4 | * @namespace com.myorg.myapp.controller
5 | */
6 | export default class App extends BaseController {
7 | public onInit(): void {
8 | // apply content density mode to root view
9 | this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/controller/BaseController.ts:
--------------------------------------------------------------------------------
1 | import Controller from "sap/ui/core/mvc/Controller";
2 | import UIComponent from "sap/ui/core/UIComponent";
3 | import AppComponent from "../Component";
4 | import Model from "sap/ui/model/Model";
5 | import ResourceModel from "sap/ui/model/resource/ResourceModel";
6 | import ResourceBundle from "sap/base/i18n/ResourceBundle";
7 | import Router from "sap/ui/core/routing/Router";
8 | import History from "sap/ui/core/routing/History";
9 |
10 | /**
11 | * @namespace com.myorg.myapp.controller
12 | */
13 | export default abstract class BaseController extends Controller {
14 | /**
15 | * Convenience method for accessing the component of the controller's view.
16 | * @returns The component of the controller's view
17 | */
18 | public getOwnerComponent(): AppComponent {
19 | return super.getOwnerComponent() as AppComponent;
20 | }
21 |
22 | /**
23 | * Convenience method to get the components' router instance.
24 | * @returns The router instance
25 | */
26 | public getRouter(): Router {
27 | return UIComponent.getRouterFor(this);
28 | }
29 |
30 | /**
31 | * Convenience method for getting the i18n resource bundle of the component.
32 | * @returns The i18n resource bundle of the component
33 | */
34 | public getResourceBundle(): ResourceBundle | Promise {
35 | const oModel = this.getOwnerComponent().getModel("i18n") as ResourceModel;
36 | return oModel.getResourceBundle();
37 | }
38 |
39 | /**
40 | * Convenience method for getting the view model by name in every controller of the application.
41 | * @param [sName] The model name
42 | * @returns The model instance
43 | */
44 | public getModel(sName?: string): Model {
45 | return this.getView().getModel(sName);
46 | }
47 |
48 | /**
49 | * Convenience method for setting the view model in every controller of the application.
50 | * @param oModel The model instance
51 | * @param [sName] The model name
52 | * @returns The current base controller instance
53 | */
54 | public setModel(oModel: Model, sName?: string): BaseController {
55 | this.getView().setModel(oModel, sName);
56 | return this;
57 | }
58 |
59 | /**
60 | * Convenience method for triggering the navigation to a specific target.
61 | * @public
62 | * @param sName Target name
63 | * @param [oParameters] Navigation parameters
64 | * @param [bReplace] Defines if the hash should be replaced (no browser history entry) or set (browser history entry)
65 | */
66 | public navTo(sName: string, oParameters?: object, bReplace?: boolean): void {
67 | this.getRouter().navTo(sName, oParameters, undefined, bReplace);
68 | }
69 |
70 | /**
71 | * Convenience event handler for navigating back.
72 | * It there is a history entry we go one step back in the browser history
73 | * If not, it will replace the current entry of the browser history with the main route.
74 | */
75 | public onNavBack(): void {
76 | const sPreviousHash = History.getInstance().getPreviousHash();
77 | if (sPreviousHash !== undefined) {
78 | window.history.go(-1);
79 | } else {
80 | this.getRouter().navTo("main", {}, undefined, true);
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/controller/Main.controller.ts:
--------------------------------------------------------------------------------
1 | import { InputBase$ChangeEvent } from "sap/m/InputBase";
2 | import BaseController from "./BaseController";
3 | import JSONModel from "sap/ui/model/json/JSONModel";
4 | import * as Nominatim from "nominatim-client";
5 | import MessageBox from "sap/m/MessageBox";
6 |
7 | type WeatherInfo = {
8 | current_weather: {
9 | temperature: number,
10 | windspeed: number,
11 | winddirection: number
12 | },
13 | placeName: string
14 | }
15 |
16 | /**
17 | * @namespace com.myorg.myapp.controller
18 | */
19 | export default class Main extends BaseController {
20 | onInit(): void {
21 | const model = new JSONModel();
22 | this.setModel(model);
23 | void this.loadWeatherData();
24 |
25 | /*
26 | // ALTERNATIVE to declarative event handler attaching in XMLView
27 | const input = this.byId("location");
28 | if (input.isA("sap.m.Input")) { // type guard (unfortunately the control class needs to be given twice)
29 | input.attachChange(function(evt) { // now TS knows input is an Input
30 | const location = evt.getParameter("value"); // type safety even for string-based access
31 | });
32 | }
33 | */
34 | }
35 |
36 | async loadWeatherData(lat = "49.31", lon = "8.64", placeName = "Walldorf") { // default coordinates: Walldorf
37 | const response = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true`);
38 | const jsonData = await response.json() as WeatherInfo;
39 | jsonData.placeName = placeName;
40 | (this.getModel() as JSONModel).setData(jsonData);
41 | }
42 |
43 | locationChange(evt: InputBase$ChangeEvent) {
44 | const location = evt.getParameters().value;
45 |
46 | Nominatim.createClient({
47 | useragent: "UI5 TypeScript Tutorial App", // useragent and referrer required by the terms of use
48 | referer: "https://localhost"
49 | }).search({q: location}).then((results) => {
50 | if (results.length > 0) {
51 | return this.loadWeatherData(results[0].lat, results[0].lon, results[0].display_name); // for simplicity just use the first/best match
52 | } else {
53 | MessageBox.alert(`Location ${location} not found`, {
54 | actions: MessageBox.Action.CLOSE // enums are now properties on the default export!
55 | });
56 | }
57 | }).catch(() => {
58 | MessageBox.alert(`Failure while searching ${location}`, {
59 | actions: MessageBox.Action.CLOSE // enums are now properties on the default export!
60 | });
61 | });
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/i18n/i18n.properties:
--------------------------------------------------------------------------------
1 | appTitle=com.myorg.myapp
2 | appDescription=UI5 Application com.myorg.myapp
3 | btnText=Say Hello
4 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/i18n/i18n_de.properties:
--------------------------------------------------------------------------------
1 | appTitle=com.myorg.myapp
2 | appDescription=UI5 Application com.myorg.myapp
3 | btnText=Sag Hallo
4 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/i18n/i18n_en.properties:
--------------------------------------------------------------------------------
1 | appTitle=com.myorg.myapp
2 | appDescription=UI5 Application com.myorg.myapp
3 | btnText=Say Hello
4 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/index-cdn.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | UI5 Application: com.myorg.myapp
11 |
12 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | UI5 Application: com.myorg.myapp
10 |
11 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "_version": "1.12.0",
3 |
4 | "sap.app": {
5 | "id": "com.myorg.myapp",
6 | "type": "application",
7 | "i18n": "i18n/i18n.properties",
8 | "title": "{{appTitle}}",
9 | "description": "{{appDescription}}",
10 | "applicationVersion": {
11 | "version": "1.0.0"
12 | }
13 | },
14 |
15 | "sap.ui": {
16 | "technology": "UI5",
17 | "icons": {},
18 | "deviceTypes": {
19 | "desktop": true,
20 | "tablet": true,
21 | "phone": true
22 | }
23 | },
24 |
25 | "sap.ui5": {
26 | "rootView": {
27 | "viewName": "com.myorg.myapp.view.App",
28 | "type": "XML",
29 | "async": true,
30 | "id": "app"
31 | },
32 |
33 | "dependencies": {
34 | "minUI5Version": "1.115.1",
35 | "libs": {
36 | "sap.ui.core": {},
37 | "sap.m": {},
38 | "sap.ui.layout": {}
39 | }
40 | },
41 |
42 | "handleValidation": true,
43 |
44 | "contentDensities": {
45 | "compact": true,
46 | "cozy": true
47 | },
48 |
49 | "models": {
50 | "i18n": {
51 | "type": "sap.ui.model.resource.ResourceModel",
52 | "settings": {
53 | "bundleName": "com.myorg.myapp.i18n.i18n"
54 | }
55 | }
56 | },
57 |
58 | "routing": {
59 | "config": {
60 | "routerClass": "sap.m.routing.Router",
61 | "viewType": "XML",
62 | "viewPath": "com.myorg.myapp.view",
63 | "controlId": "app",
64 | "controlAggregation": "pages",
65 | "async": true
66 | },
67 | "routes": [
68 | {
69 | "pattern": "",
70 | "name": "main",
71 | "target": "main"
72 | }
73 | ],
74 | "targets": {
75 | "main": {
76 | "viewId": "main",
77 | "viewName": "Main"
78 | }
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/model/formatter.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | formatValue: (value: string) => {
3 | return value?.toUpperCase();
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/model/models.ts:
--------------------------------------------------------------------------------
1 | import JSONModel from "sap/ui/model/json/JSONModel";
2 | import BindingMode from "sap/ui/model/BindingMode";
3 |
4 | import Device from "sap/ui/Device";
5 |
6 |
7 | export default {
8 | createDeviceModel: () => {
9 | const oModel = new JSONModel(Device);
10 | oModel.setDefaultBindingMode(BindingMode.OneWay);
11 | return oModel;
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/test/Test.qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/test/integration/HelloJourney.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-floating-promises */
2 | import opaTest from "sap/ui/test/opaQunit";
3 | import MainPage from "./pages/MainPage";
4 |
5 | const onTheMainPage = new MainPage();
6 |
7 | QUnit.module("Sample Hello Journey");
8 |
9 | opaTest("Should open the Hello dialog", function () {
10 | // Arrangements
11 | onTheMainPage.iStartMyUIComponent({
12 | componentConfig: {
13 | name: "com.myorg.myapp"
14 | }
15 | });
16 |
17 | // Actions
18 | onTheMainPage.iPressTheSayHelloWithDialogButton();
19 |
20 | // Assertions
21 | onTheMainPage.iShouldSeeTheHelloDialog();
22 |
23 | // Actions
24 | onTheMainPage.iPressTheOkButtonInTheDialog();
25 |
26 | // Assertions
27 | onTheMainPage.iShouldNotSeeTheHelloDialog();
28 |
29 | // Cleanup
30 | onTheMainPage.iTeardownMyApp();
31 | });
32 |
33 | opaTest("Should close the Hello dialog", function () {
34 | // Arrangements
35 | onTheMainPage.iStartMyUIComponent({
36 | componentConfig: {
37 | name: "com.myorg.myapp"
38 | }
39 | });
40 |
41 | // Actions
42 | onTheMainPage.iPressTheSayHelloWithDialogButton();
43 | onTheMainPage.iPressTheOkButtonInTheDialog();
44 |
45 | // Assertions
46 | onTheMainPage.iShouldNotSeeTheHelloDialog();
47 |
48 | // Cleanup
49 | onTheMainPage.iTeardownMyApp();
50 | });
51 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/test/integration/opaTests.qunit.ts:
--------------------------------------------------------------------------------
1 | import "./HelloJourney";
2 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/test/integration/pages/MainPage.ts:
--------------------------------------------------------------------------------
1 | import Opa5 from "sap/ui/test/Opa5";
2 | import Press from "sap/ui/test/actions/Press";
3 |
4 | const viewName = "com.myorg.myapp.view.Main";
5 |
6 | export default class MainPage extends Opa5 {
7 | // Actions
8 | iPressTheSayHelloWithDialogButton() {
9 | this.waitFor({
10 | id: "helloButton",
11 | viewName,
12 | actions: new Press(),
13 | errorMessage: "Did not find the 'Say Hello With Dialog' button on the Main view"
14 | });
15 | }
16 |
17 | iPressTheOkButtonInTheDialog() {
18 | this.waitFor({
19 | controlType: "sap.m.Button",
20 | searchOpenDialogs: true,
21 | viewName,
22 | actions: new Press(),
23 | errorMessage: "Did not find the 'OK' button in the Dialog"
24 | });
25 | }
26 |
27 | // Assertions
28 | iShouldSeeTheHelloDialog() {
29 | this.waitFor({
30 | controlType: "sap.m.Dialog",
31 | success: function () {
32 | // we set the view busy, so we need to query the parent of the app
33 | Opa5.assert.ok(true, "The dialog is open");
34 | },
35 | errorMessage: "Did not find the dialog control"
36 | });
37 | }
38 |
39 | iShouldNotSeeTheHelloDialog() {
40 | this.waitFor({
41 | controlType: "sap.m.App", // dummy, I just want a check function, where I can search the DOM. Probably there is a better way for a NEGATIVE test (NO dialog).
42 | check: function () {
43 | return document.querySelectorAll(".sapMDialog").length === 0;
44 | },
45 | success: function () {
46 | Opa5.assert.ok(true, "No dialog is open");
47 | }
48 | });
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/test/testsuite.qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/test/testsuite.qunit.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "Unit test suite for the UI5 Application: com.myorg.myapp",
3 | defaults: {
4 | page: "ui5://test-resources/com/myorg/myapp/Test.qunit.html?testsuite={suite}&test={name}",
5 | qunit: {
6 | version: 2
7 | },
8 | ui5: {
9 | theme: "sap_horizon"
10 | },
11 | loader: {
12 | paths: {
13 | "com/myorg/myapp": "../"
14 | }
15 | }
16 | },
17 | tests: {
18 | "unit/unitTests": {
19 | title: "Unit tests for the UI5 Application: com.myorg.myapp"
20 | },
21 | "integration/opaTests": {
22 | title: "Integration tests for the UI5 Application: com.myorg.myapp"
23 | }
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/test/unit/controller/Main.qunit.ts:
--------------------------------------------------------------------------------
1 | import Main from "com/myorg/myapp/controller/Main.controller";
2 |
3 | QUnit.module("Sample Main controller test");
4 |
5 | QUnit.test("The Main controller class has a sayHello method", function (assert) {
6 | // as a very basic test example just check the presence of the "sayHello" method
7 | assert.strictEqual(typeof Main.prototype.sayHello, "function");
8 | });
9 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/test/unit/unitTests.qunit.ts:
--------------------------------------------------------------------------------
1 | import "./controller/Main.qunit";
2 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/view/App.view.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/exercises/ex4/com.myorg.myapp/webapp/view/Main.view.xml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/exercises/ex4/images/ex4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SAP-samples/ui5-typescript-tutorial/HEAD/exercises/ex4/images/ex4.png
--------------------------------------------------------------------------------
/exercises/ex4/images/nominatim_createClient.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SAP-samples/ui5-typescript-tutorial/HEAD/exercises/ex4/images/nominatim_createClient.png
--------------------------------------------------------------------------------
/exercises/ex4/images/placeName_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SAP-samples/ui5-typescript-tutorial/HEAD/exercises/ex4/images/placeName_error.png
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | # We recommend you to keep these unchanged
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 |
14 | # Change these settings to your own preference
15 | indent_style = tab
16 | indent_size = 2
17 |
18 | [*.{yaml,yml}]
19 | indent_style = space
20 |
21 | [*.md]
22 | indent_style = unset
23 | trim_trailing_whitespace = false
24 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "root": true,
3 | "env": {
4 | "browser": true,
5 | "es6": true,
6 | "node": true
7 | },
8 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking"],
9 | "parser": "@typescript-eslint/parser",
10 | "parserOptions": {
11 | "project": [`${__dirname}/tsconfig.json`],
12 | "sourceType": "module"
13 | },
14 | "plugins": ["@typescript-eslint"],
15 | "ignorePatterns": [".eslintrc.js"]
16 | };
17 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/.gitignore:
--------------------------------------------------------------------------------
1 | # build results
2 | dist
3 | coverage
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 |
12 | # Dependency directories
13 | node_modules/
14 |
15 | .DS_Store
16 | .env
17 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/.yo-rc.json:
--------------------------------------------------------------------------------
1 | {
2 | "generator-ui5-ts-app": {
3 | "namespace": "com.myorg.myapp",
4 | "framework": "OpenUI5",
5 | "frameworkVersion": "1.115.1",
6 | "author": "OpenUI5 Team",
7 | "initrepo": false,
8 | "tstypes": "@openui5/types",
9 | "tstypesVersion": "1.115.1",
10 | "appId": "com.myorg.myapp",
11 | "appURI": "com/myorg/myapp",
12 | "cdnDomain": "sdk.openui5.org",
13 | "defaultTheme": "sap_horizon",
14 | "gte11150": true,
15 | "setupCompleted": true
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/karma-ci-cov.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | require("./karma-ci.conf")(config);
3 | config.set({
4 | reporters: ["progress", "coverage"],
5 | preprocessors: {
6 | "webapp/**/*.ts": ["ui5-transpile"]
7 | },
8 | coverageReporter: {
9 | dir: "coverage",
10 | reporters: [
11 | { type: "html", subdir: "report-html" },
12 | { type: "cobertura", subdir: ".", file: "cobertura.txt" },
13 | { type: "lcovonly", subdir: ".", file: "report-lcovonly.txt" },
14 | { type: "text-summary" }
15 | ]
16 | }
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/karma-ci.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | require("./karma.conf")(config);
3 | config.set({
4 | browsers: ["ChromeHeadless"],
5 | singleRun: true
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/karma.conf.js:
--------------------------------------------------------------------------------
1 | // karma-ui5 usage: https://github.com/SAP/karma-ui5
2 | module.exports = function (config) {
3 | config.set({
4 | frameworks: ["ui5"],
5 | browsers: ["Chrome"]
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "com.myorg.myapp",
3 | "version": "1.0.0",
4 | "description": "UI5 Application: com.myorg.myapp",
5 | "author": "OpenUI5 Team",
6 | "license": "Apache-2.0",
7 | "scripts": {
8 | "clean": "rimraf dist coverage",
9 | "build": "ui5 build --clean-dest",
10 | "build:opt": "ui5 build self-contained --clean-dest --all",
11 | "start": "ui5 serve --port 8080 -o index.html",
12 | "start-cdn": "ui5 serve --port 8080 -o index-cdn.html",
13 | "start:dist": "npm start -- --config ui5-dist.yaml",
14 | "start:dist-cdn": "npm run start-cdn -- --config ui5-dist.yaml",
15 | "ts-typecheck": "tsc --noEmit",
16 | "lint": "eslint webapp",
17 | "karma": "karma start",
18 | "karma-ci": "karma start karma-ci.conf.js",
19 | "karma-ci-cov": "karma start karma-ci-cov.conf.js",
20 | "test": "npm run lint && npm run karma-ci-cov"
21 | },
22 | "devDependencies": {
23 | "@openui5/types": "1.131.1",
24 | "@typescript-eslint/eslint-plugin": "^5.61.0",
25 | "@typescript-eslint/parser": "^5.61.0",
26 | "@ui5/cli": "^4.0.11",
27 | "@ui5/ts-interface-generator": "^0.7.0",
28 | "eslint": "^8.44.0",
29 | "karma": "^6.4.2",
30 | "karma-chrome-launcher": "^3.2.0",
31 | "karma-coverage": "^2.2.1",
32 | "karma-ui5": "^3.0.3",
33 | "karma-ui5-transpile": "^0.3.22",
34 | "nominatim-client": "^3.2.1",
35 | "rimraf": "^5.0.1",
36 | "typescript": "^5.1.6",
37 | "ui5-middleware-livereload": "^0.8.4",
38 | "ui5-tooling-modules": "^0.9.12",
39 | "ui5-tooling-transpile": "^0.7.17"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2022",
4 | "module": "es2022",
5 | "moduleResolution": "node",
6 | "skipLibCheck": true,
7 | "allowJs": true,
8 | "strict": true,
9 | "strictNullChecks": false,
10 | "strictPropertyInitialization": false,
11 | "rootDir": "./webapp",
12 | "types": ["@openui5/types", "@types/qunit"],
13 | "paths": {
14 | "com/myorg/myapp/*": ["./webapp/*"],
15 | "unit/*": ["./webapp/test/unit/*"],
16 | "integration/*": ["./webapp/test/integration/*"]
17 | }
18 | },
19 | "include": ["./webapp/**/*"],
20 | "exclude": ["./webapp/coverage/**/*"]
21 | }
22 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/ui5-dist.yaml:
--------------------------------------------------------------------------------
1 | specVersion: "3.0"
2 | metadata:
3 | name: com.myorg.myapp
4 | type: application
5 | resources:
6 | configuration:
7 | paths:
8 | webapp: dist
9 | framework:
10 | name: OpenUI5
11 | version: "1.131.1"
12 | libraries:
13 | - name: sap.m
14 | - name: sap.ui.core
15 | - name: sap.ui.layout
16 | - name: themelib_sap_horizon
17 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/ui5.yaml:
--------------------------------------------------------------------------------
1 | specVersion: "3.0"
2 | metadata:
3 | name: com.myorg.myapp
4 | type: application
5 | framework:
6 | name: OpenUI5
7 | version: "1.131.1"
8 | libraries:
9 | - name: sap.m
10 | - name: sap.ui.core
11 | - name: sap.ui.layout
12 | - name: themelib_sap_horizon
13 | builder:
14 | customTasks:
15 | - name: ui5-tooling-modules-task
16 | afterTask: replaceVersion
17 | configuration:
18 | addToNamespace: true
19 | - name: ui5-tooling-transpile-task
20 | afterTask: replaceVersion
21 | server:
22 | customMiddleware:
23 | - name: ui5-tooling-modules-middleware
24 | afterMiddleware: compression
25 | - name: ui5-tooling-transpile-middleware
26 | afterMiddleware: compression
27 | - name: ui5-middleware-livereload
28 | afterMiddleware: compression
29 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/Component.ts:
--------------------------------------------------------------------------------
1 | import UIComponent from "sap/ui/core/UIComponent";
2 | import models from "./model/models";
3 | import Device from "sap/ui/Device";
4 |
5 | /**
6 | * @namespace com.myorg.myapp
7 | */
8 | export default class Component extends UIComponent {
9 | public static metadata = {
10 | manifest: "json",
11 | };
12 |
13 | private contentDensityClass: string;
14 |
15 | public init(): void {
16 | // call the base component's init function
17 | super.init();
18 |
19 | // create the device model
20 | this.setModel(models.createDeviceModel(), "device");
21 |
22 | // create the views based on the url/hash
23 | this.getRouter().initialize();
24 | }
25 |
26 | /**
27 | * This method can be called to determine whether the sapUiSizeCompact or sapUiSizeCozy
28 | * design mode class should be set, which influences the size appearance of some controls.
29 | * @public
30 | * @returns css class, either 'sapUiSizeCompact' or 'sapUiSizeCozy' - or an empty string if no css class should be set
31 | */
32 | public getContentDensityClass(): string {
33 | if (this.contentDensityClass === undefined) {
34 | // check whether FLP has already set the content density class; do nothing in this case
35 | if (document.body.classList.contains("sapUiSizeCozy") || document.body.classList.contains("sapUiSizeCompact")) {
36 | this.contentDensityClass = "";
37 | } else if (!Device.support.touch) {
38 | // apply "compact" mode if touch is not supported
39 | this.contentDensityClass = "sapUiSizeCompact";
40 | } else {
41 | // "cozy" in case of touch support; default for most sap.m controls, but needed for desktop-first controls like sap.ui.table.Table
42 | this.contentDensityClass = "sapUiSizeCozy";
43 | }
44 | }
45 | return this.contentDensityClass;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/control/WindDirection.gen.d.ts:
--------------------------------------------------------------------------------
1 | import { PropertyBindingInfo } from "sap/ui/base/ManagedObject";
2 | import { $ControlSettings } from "sap/ui/core/Control";
3 |
4 | declare module "./WindDirection" {
5 |
6 | /**
7 | * Interface defining the settings object used in constructor calls
8 | */
9 | interface $WindDirectionSettings extends $ControlSettings {
10 |
11 | /**
12 | * The direction in degrees FROM which the wind blows (this is the internationally common definition). Value 0 means: wind blows from North to South.
13 | */
14 | direction?: number | PropertyBindingInfo | `{${string}}`;
15 | }
16 |
17 | export default interface WindDirection {
18 |
19 | // property: direction
20 |
21 | /**
22 | * Gets current value of property "direction".
23 | *
24 | * The direction in degrees FROM which the wind blows (this is the internationally common definition). Value 0 means: wind blows from North to South.
25 | *
26 | * @returns Value of property "direction"
27 | */
28 | getDirection(): number;
29 |
30 | /**
31 | * Sets a new value for property "direction".
32 | *
33 | * The direction in degrees FROM which the wind blows (this is the internationally common definition). Value 0 means: wind blows from North to South.
34 | *
35 | * When called with a value of "null" or "undefined", the default value of the property will be restored.
36 | *
37 | * @param direction New value for property "direction"
38 | * @returns Reference to "this" in order to allow method chaining
39 | */
40 | setDirection(direction: number): this;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/control/WindDirection.ts:
--------------------------------------------------------------------------------
1 | import Control from "sap/ui/core/Control";
2 | import RenderManager from "sap/ui/core/RenderManager";
3 | import type { MetadataOptions } from "sap/ui/core/Element";
4 |
5 | /**
6 | * @namespace com.myorg.myapp.control
7 | */
8 | export default class WindDirection extends Control {
9 |
10 | // The following three lines were generated and should remain as-is to make TypeScript aware of the constructor signatures
11 | constructor(idOrSettings?: string | $WindDirectionSettings);
12 | constructor(id?: string, settings?: $WindDirectionSettings);
13 | constructor(id?: string, settings?: $WindDirectionSettings) { super(id, settings); }
14 |
15 | static readonly metadata: MetadataOptions = {
16 | properties: {
17 | /**
18 | * The direction in degrees FROM which the wind blows (this is the internationally common definition). Value 0 means: wind blows from North to South.
19 | */
20 | "direction": "float"
21 | }
22 | }
23 |
24 | renderer = {
25 | apiVersion: 2,
26 | render: (rm: RenderManager, control: WindDirection) => {
27 | rm.openStart("div", control);
28 | rm.style("font-size", "2rem");
29 | rm.style("width", "2rem");
30 | rm.style("height", "2rem");
31 | rm.style("display", "inline-block");
32 | rm.style("color", "blue");
33 | rm.style("transform-origin", "center");
34 | rm.style("transform", `rotate(${control.getDirection() + 90}deg)`); // arrow is pointing right by default, direction 0 means blowing FROM the north, so the arrow has to point down
35 | rm.openEnd();
36 | rm.text("➢");
37 | rm.close("div");
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/controller/App.controller.ts:
--------------------------------------------------------------------------------
1 | import BaseController from "./BaseController";
2 |
3 | /**
4 | * @namespace com.myorg.myapp.controller
5 | */
6 | export default class App extends BaseController {
7 | public onInit(): void {
8 | // apply content density mode to root view
9 | this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/controller/BaseController.ts:
--------------------------------------------------------------------------------
1 | import Controller from "sap/ui/core/mvc/Controller";
2 | import UIComponent from "sap/ui/core/UIComponent";
3 | import AppComponent from "../Component";
4 | import Model from "sap/ui/model/Model";
5 | import ResourceModel from "sap/ui/model/resource/ResourceModel";
6 | import ResourceBundle from "sap/base/i18n/ResourceBundle";
7 | import Router from "sap/ui/core/routing/Router";
8 | import History from "sap/ui/core/routing/History";
9 |
10 | /**
11 | * @namespace com.myorg.myapp.controller
12 | */
13 | export default abstract class BaseController extends Controller {
14 | /**
15 | * Convenience method for accessing the component of the controller's view.
16 | * @returns The component of the controller's view
17 | */
18 | public getOwnerComponent(): AppComponent {
19 | return super.getOwnerComponent() as AppComponent;
20 | }
21 |
22 | /**
23 | * Convenience method to get the components' router instance.
24 | * @returns The router instance
25 | */
26 | public getRouter(): Router {
27 | return UIComponent.getRouterFor(this);
28 | }
29 |
30 | /**
31 | * Convenience method for getting the i18n resource bundle of the component.
32 | * @returns The i18n resource bundle of the component
33 | */
34 | public getResourceBundle(): ResourceBundle | Promise {
35 | const oModel = this.getOwnerComponent().getModel("i18n") as ResourceModel;
36 | return oModel.getResourceBundle();
37 | }
38 |
39 | /**
40 | * Convenience method for getting the view model by name in every controller of the application.
41 | * @param [sName] The model name
42 | * @returns The model instance
43 | */
44 | public getModel(sName?: string): Model {
45 | return this.getView().getModel(sName);
46 | }
47 |
48 | /**
49 | * Convenience method for setting the view model in every controller of the application.
50 | * @param oModel The model instance
51 | * @param [sName] The model name
52 | * @returns The current base controller instance
53 | */
54 | public setModel(oModel: Model, sName?: string): BaseController {
55 | this.getView().setModel(oModel, sName);
56 | return this;
57 | }
58 |
59 | /**
60 | * Convenience method for triggering the navigation to a specific target.
61 | * @public
62 | * @param sName Target name
63 | * @param [oParameters] Navigation parameters
64 | * @param [bReplace] Defines if the hash should be replaced (no browser history entry) or set (browser history entry)
65 | */
66 | public navTo(sName: string, oParameters?: object, bReplace?: boolean): void {
67 | this.getRouter().navTo(sName, oParameters, undefined, bReplace);
68 | }
69 |
70 | /**
71 | * Convenience event handler for navigating back.
72 | * It there is a history entry we go one step back in the browser history
73 | * If not, it will replace the current entry of the browser history with the main route.
74 | */
75 | public onNavBack(): void {
76 | const sPreviousHash = History.getInstance().getPreviousHash();
77 | if (sPreviousHash !== undefined) {
78 | window.history.go(-1);
79 | } else {
80 | this.getRouter().navTo("main", {}, undefined, true);
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/controller/Main.controller.ts:
--------------------------------------------------------------------------------
1 | import { InputBase$ChangeEvent } from "sap/m/InputBase";
2 | import BaseController from "./BaseController";
3 | import JSONModel from "sap/ui/model/json/JSONModel";
4 | import * as Nominatim from "nominatim-client";
5 | import MessageBox from "sap/m/MessageBox";
6 |
7 | type WeatherInfo = {
8 | current_weather: {
9 | temperature: number,
10 | windspeed: number,
11 | winddirection: number
12 | },
13 | placeName: string
14 | }
15 |
16 | /**
17 | * @namespace com.myorg.myapp.controller
18 | */
19 | export default class Main extends BaseController {
20 | onInit(): void {
21 | const model = new JSONModel();
22 | this.setModel(model);
23 | void this.loadWeatherData();
24 |
25 | /*
26 | // ALTERNATIVE to declarative event handler attaching in XMLView
27 | const input = this.byId("location");
28 | if (input.isA("sap.m.Input")) { // type guard (unfortunately the control class needs to be given twice)
29 | input.attachChange(function(evt) { // now TS knows input is an Input
30 | const location = evt.getParameter("value"); // type safety even for string-based access
31 | });
32 | }
33 | */
34 | }
35 |
36 | async loadWeatherData(lat = "49.31", lon = "8.64", placeName = "Walldorf") { // default coordinates: Walldorf
37 | const response = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true`);
38 | const jsonData = await response.json() as WeatherInfo;
39 | jsonData.placeName = placeName;
40 | (this.getModel() as JSONModel).setData(jsonData);
41 | }
42 |
43 | locationChange(evt: InputBase$ChangeEvent) {
44 | const location = evt.getParameters().value;
45 |
46 | Nominatim.createClient({
47 | useragent: "UI5 TypeScript Tutorial App", // useragent and referrer required by the terms of use
48 | referer: "https://localhost"
49 | }).search({q: location}).then((results) => {
50 | if (results.length > 0) {
51 | return this.loadWeatherData(results[0].lat, results[0].lon, results[0].display_name); // for simplicity just use the first/best match
52 | } else {
53 | MessageBox.alert(`Location ${location} not found`, {
54 | actions: MessageBox.Action.CLOSE // enums are now properties on the default export!
55 | });
56 | }
57 | }).catch(() => {
58 | MessageBox.alert(`Failure while searching ${location}`, {
59 | actions: MessageBox.Action.CLOSE // enums are now properties on the default export!
60 | });
61 | });
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/i18n/i18n.properties:
--------------------------------------------------------------------------------
1 | appTitle=com.myorg.myapp
2 | appDescription=UI5 Application com.myorg.myapp
3 | btnText=Say Hello
4 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/i18n/i18n_de.properties:
--------------------------------------------------------------------------------
1 | appTitle=com.myorg.myapp
2 | appDescription=UI5 Application com.myorg.myapp
3 | btnText=Sag Hallo
4 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/i18n/i18n_en.properties:
--------------------------------------------------------------------------------
1 | appTitle=com.myorg.myapp
2 | appDescription=UI5 Application com.myorg.myapp
3 | btnText=Say Hello
4 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/index-cdn.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | UI5 Application: com.myorg.myapp
11 |
12 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | UI5 Application: com.myorg.myapp
10 |
11 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "_version": "1.12.0",
3 |
4 | "sap.app": {
5 | "id": "com.myorg.myapp",
6 | "type": "application",
7 | "i18n": "i18n/i18n.properties",
8 | "title": "{{appTitle}}",
9 | "description": "{{appDescription}}",
10 | "applicationVersion": {
11 | "version": "1.0.0"
12 | }
13 | },
14 |
15 | "sap.ui": {
16 | "technology": "UI5",
17 | "icons": {},
18 | "deviceTypes": {
19 | "desktop": true,
20 | "tablet": true,
21 | "phone": true
22 | }
23 | },
24 |
25 | "sap.ui5": {
26 | "rootView": {
27 | "viewName": "com.myorg.myapp.view.App",
28 | "type": "XML",
29 | "async": true,
30 | "id": "app"
31 | },
32 |
33 | "dependencies": {
34 | "minUI5Version": "1.115.1",
35 | "libs": {
36 | "sap.ui.core": {},
37 | "sap.m": {},
38 | "sap.ui.layout": {}
39 | }
40 | },
41 |
42 | "handleValidation": true,
43 |
44 | "contentDensities": {
45 | "compact": true,
46 | "cozy": true
47 | },
48 |
49 | "models": {
50 | "i18n": {
51 | "type": "sap.ui.model.resource.ResourceModel",
52 | "settings": {
53 | "bundleName": "com.myorg.myapp.i18n.i18n"
54 | }
55 | }
56 | },
57 |
58 | "routing": {
59 | "config": {
60 | "routerClass": "sap.m.routing.Router",
61 | "viewType": "XML",
62 | "viewPath": "com.myorg.myapp.view",
63 | "controlId": "app",
64 | "controlAggregation": "pages",
65 | "async": true
66 | },
67 | "routes": [
68 | {
69 | "pattern": "",
70 | "name": "main",
71 | "target": "main"
72 | }
73 | ],
74 | "targets": {
75 | "main": {
76 | "viewId": "main",
77 | "viewName": "Main"
78 | }
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/model/formatter.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | formatValue: (value: string) => {
3 | return value?.toUpperCase();
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/model/models.ts:
--------------------------------------------------------------------------------
1 | import JSONModel from "sap/ui/model/json/JSONModel";
2 | import BindingMode from "sap/ui/model/BindingMode";
3 |
4 | import Device from "sap/ui/Device";
5 |
6 |
7 | export default {
8 | createDeviceModel: () => {
9 | const oModel = new JSONModel(Device);
10 | oModel.setDefaultBindingMode(BindingMode.OneWay);
11 | return oModel;
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/test/Test.qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/test/integration/HelloJourney.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-floating-promises */
2 | import opaTest from "sap/ui/test/opaQunit";
3 | import MainPage from "./pages/MainPage";
4 |
5 | const onTheMainPage = new MainPage();
6 |
7 | QUnit.module("Sample Hello Journey");
8 |
9 | opaTest("Should open the Hello dialog", function () {
10 | // Arrangements
11 | onTheMainPage.iStartMyUIComponent({
12 | componentConfig: {
13 | name: "com.myorg.myapp"
14 | }
15 | });
16 |
17 | // Actions
18 | onTheMainPage.iPressTheSayHelloWithDialogButton();
19 |
20 | // Assertions
21 | onTheMainPage.iShouldSeeTheHelloDialog();
22 |
23 | // Actions
24 | onTheMainPage.iPressTheOkButtonInTheDialog();
25 |
26 | // Assertions
27 | onTheMainPage.iShouldNotSeeTheHelloDialog();
28 |
29 | // Cleanup
30 | onTheMainPage.iTeardownMyApp();
31 | });
32 |
33 | opaTest("Should close the Hello dialog", function () {
34 | // Arrangements
35 | onTheMainPage.iStartMyUIComponent({
36 | componentConfig: {
37 | name: "com.myorg.myapp"
38 | }
39 | });
40 |
41 | // Actions
42 | onTheMainPage.iPressTheSayHelloWithDialogButton();
43 | onTheMainPage.iPressTheOkButtonInTheDialog();
44 |
45 | // Assertions
46 | onTheMainPage.iShouldNotSeeTheHelloDialog();
47 |
48 | // Cleanup
49 | onTheMainPage.iTeardownMyApp();
50 | });
51 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/test/integration/opaTests.qunit.ts:
--------------------------------------------------------------------------------
1 | import "./HelloJourney";
2 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/test/integration/pages/MainPage.ts:
--------------------------------------------------------------------------------
1 | import Opa5 from "sap/ui/test/Opa5";
2 | import Press from "sap/ui/test/actions/Press";
3 |
4 | const viewName = "com.myorg.myapp.view.Main";
5 |
6 | export default class MainPage extends Opa5 {
7 | // Actions
8 | iPressTheSayHelloWithDialogButton() {
9 | this.waitFor({
10 | id: "helloButton",
11 | viewName,
12 | actions: new Press(),
13 | errorMessage: "Did not find the 'Say Hello With Dialog' button on the Main view"
14 | });
15 | }
16 |
17 | iPressTheOkButtonInTheDialog() {
18 | this.waitFor({
19 | controlType: "sap.m.Button",
20 | searchOpenDialogs: true,
21 | viewName,
22 | actions: new Press(),
23 | errorMessage: "Did not find the 'OK' button in the Dialog"
24 | });
25 | }
26 |
27 | // Assertions
28 | iShouldSeeTheHelloDialog() {
29 | this.waitFor({
30 | controlType: "sap.m.Dialog",
31 | success: function () {
32 | // we set the view busy, so we need to query the parent of the app
33 | Opa5.assert.ok(true, "The dialog is open");
34 | },
35 | errorMessage: "Did not find the dialog control"
36 | });
37 | }
38 |
39 | iShouldNotSeeTheHelloDialog() {
40 | this.waitFor({
41 | controlType: "sap.m.App", // dummy, I just want a check function, where I can search the DOM. Probably there is a better way for a NEGATIVE test (NO dialog).
42 | check: function () {
43 | return document.querySelectorAll(".sapMDialog").length === 0;
44 | },
45 | success: function () {
46 | Opa5.assert.ok(true, "No dialog is open");
47 | }
48 | });
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/test/testsuite.qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/test/testsuite.qunit.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "Unit test suite for the UI5 Application: com.myorg.myapp",
3 | defaults: {
4 | page: "ui5://test-resources/com/myorg/myapp/Test.qunit.html?testsuite={suite}&test={name}",
5 | qunit: {
6 | version: 2
7 | },
8 | ui5: {
9 | theme: "sap_horizon"
10 | },
11 | loader: {
12 | paths: {
13 | "com/myorg/myapp": "../"
14 | }
15 | }
16 | },
17 | tests: {
18 | "unit/unitTests": {
19 | title: "Unit tests for the UI5 Application: com.myorg.myapp"
20 | },
21 | "integration/opaTests": {
22 | title: "Integration tests for the UI5 Application: com.myorg.myapp"
23 | }
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/test/unit/controller/Main.qunit.ts:
--------------------------------------------------------------------------------
1 | import Main from "com/myorg/myapp/controller/Main.controller";
2 |
3 | QUnit.module("Sample Main controller test");
4 |
5 | QUnit.test("The Main controller class has a sayHello method", function (assert) {
6 | // as a very basic test example just check the presence of the "sayHello" method
7 | assert.strictEqual(typeof Main.prototype.sayHello, "function");
8 | });
9 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/test/unit/unitTests.qunit.ts:
--------------------------------------------------------------------------------
1 | import "./controller/Main.qunit";
2 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/view/App.view.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/exercises/ex5/com.myorg.myapp/webapp/view/Main.view.xml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/exercises/ex5/images/Constructor_Suggestions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SAP-samples/ui5-typescript-tutorial/HEAD/exercises/ex5/images/Constructor_Suggestions.png
--------------------------------------------------------------------------------
/exercises/ex5/images/getDirection_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SAP-samples/ui5-typescript-tutorial/HEAD/exercises/ex5/images/getDirection_error.png
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | # We recommend you to keep these unchanged
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 |
14 | # Change these settings to your own preference
15 | indent_style = tab
16 | indent_size = 2
17 |
18 | [*.{yaml,yml}]
19 | indent_style = space
20 |
21 | [*.md]
22 | indent_style = unset
23 | trim_trailing_whitespace = false
24 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "root": true,
3 | "env": {
4 | "browser": true,
5 | "es6": true,
6 | "node": true
7 | },
8 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking"],
9 | "parser": "@typescript-eslint/parser",
10 | "parserOptions": {
11 | "project": [`${__dirname}/tsconfig.json`],
12 | "sourceType": "module"
13 | },
14 | "plugins": ["@typescript-eslint"],
15 | "ignorePatterns": [".eslintrc.js"]
16 | };
17 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/.gitignore:
--------------------------------------------------------------------------------
1 | # build results
2 | dist
3 | coverage
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 |
12 | # Dependency directories
13 | node_modules/
14 |
15 | .DS_Store
16 | .env
17 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/.yo-rc.json:
--------------------------------------------------------------------------------
1 | {
2 | "generator-ui5-ts-app": {
3 | "namespace": "com.myorg.myapp",
4 | "framework": "OpenUI5",
5 | "frameworkVersion": "1.115.1",
6 | "author": "OpenUI5 Team",
7 | "initrepo": false,
8 | "tstypes": "@openui5/types",
9 | "tstypesVersion": "1.115.1",
10 | "appId": "com.myorg.myapp",
11 | "appURI": "com/myorg/myapp",
12 | "cdnDomain": "sdk.openui5.org",
13 | "defaultTheme": "sap_horizon",
14 | "gte11150": true,
15 | "setupCompleted": true
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/karma-ci-cov.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | require("./karma-ci.conf")(config);
3 | config.set({
4 | reporters: ["progress", "coverage"],
5 | preprocessors: {
6 | "webapp/**/*.ts": ["ui5-transpile"]
7 | },
8 | coverageReporter: {
9 | dir: "coverage",
10 | reporters: [
11 | { type: "html", subdir: "report-html" },
12 | { type: "cobertura", subdir: ".", file: "cobertura.txt" },
13 | { type: "lcovonly", subdir: ".", file: "report-lcovonly.txt" },
14 | { type: "text-summary" }
15 | ]
16 | }
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/karma-ci.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | require("./karma.conf")(config);
3 | config.set({
4 | browsers: ["ChromeHeadless"],
5 | singleRun: true
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/karma.conf.js:
--------------------------------------------------------------------------------
1 | // karma-ui5 usage: https://github.com/SAP/karma-ui5
2 | module.exports = function (config) {
3 | config.set({
4 | frameworks: ["ui5"],
5 | browsers: ["Chrome"]
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "com.myorg.myapp",
3 | "version": "1.0.0",
4 | "description": "UI5 Application: com.myorg.myapp",
5 | "author": "OpenUI5 Team",
6 | "license": "Apache-2.0",
7 | "scripts": {
8 | "clean": "rimraf dist coverage",
9 | "build": "ui5 build --clean-dest",
10 | "build:opt": "ui5 build self-contained --clean-dest --all",
11 | "start": "ui5 serve --port 8080 -o index.html",
12 | "start-cdn": "ui5 serve --port 8080 -o index-cdn.html",
13 | "start:dist": "npm start -- --config ui5-dist.yaml",
14 | "start:dist-cdn": "npm run start-cdn -- --config ui5-dist.yaml",
15 | "ts-typecheck": "tsc --noEmit",
16 | "lint": "eslint webapp",
17 | "karma": "karma start",
18 | "karma-ci": "karma start karma-ci.conf.js",
19 | "karma-ci-cov": "karma start karma-ci-cov.conf.js",
20 | "test": "npm run lint && npm run karma-ci-cov"
21 | },
22 | "devDependencies": {
23 | "@openui5/types": "1.115.1",
24 | "@typescript-eslint/eslint-plugin": "^5.61.0",
25 | "@typescript-eslint/parser": "^5.61.0",
26 | "@ui5/cli": "^4.0.11",
27 | "@ui5/ts-interface-generator": "^0.7.0",
28 | "eslint": "^8.44.0",
29 | "karma": "^6.4.2",
30 | "karma-chrome-launcher": "^3.2.0",
31 | "karma-coverage": "^2.2.1",
32 | "karma-ui5": "^3.0.3",
33 | "karma-ui5-transpile": "^0.3.22",
34 | "nominatim-client": "^3.2.1",
35 | "rimraf": "^5.0.1",
36 | "typescript": "^5.1.6",
37 | "ui5-middleware-livereload": "^0.8.4",
38 | "ui5-tooling-modules": "^0.9.12",
39 | "ui5-tooling-transpile": "^0.7.17"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2022",
4 | "module": "es2022",
5 | "moduleResolution": "node",
6 | "skipLibCheck": true,
7 | "allowJs": true,
8 | "strict": true,
9 | "strictNullChecks": false,
10 | "strictPropertyInitialization": false,
11 | "rootDir": "./webapp",
12 | "types": ["@openui5/types", "@types/qunit"],
13 | "paths": {
14 | "com/myorg/myapp/*": ["./webapp/*"],
15 | "unit/*": ["./webapp/test/unit/*"],
16 | "integration/*": ["./webapp/test/integration/*"]
17 | }
18 | },
19 | "include": ["./webapp/**/*"],
20 | "exclude": ["./webapp/coverage/**/*"]
21 | }
22 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/ui5-dist.yaml:
--------------------------------------------------------------------------------
1 | specVersion: "3.0"
2 | metadata:
3 | name: com.myorg.myapp
4 | type: application
5 | resources:
6 | configuration:
7 | paths:
8 | webapp: dist
9 | framework:
10 | name: OpenUI5
11 | version: "1.131.1"
12 | libraries:
13 | - name: sap.m
14 | - name: sap.ui.core
15 | - name: sap.ui.layout
16 | - name: themelib_sap_horizon
17 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/ui5.yaml:
--------------------------------------------------------------------------------
1 | specVersion: "3.0"
2 | metadata:
3 | name: com.myorg.myapp
4 | type: application
5 | framework:
6 | name: OpenUI5
7 | version: "1.131.1"
8 | libraries:
9 | - name: sap.m
10 | - name: sap.ui.core
11 | - name: sap.ui.layout
12 | - name: themelib_sap_horizon
13 | builder:
14 | customTasks:
15 | - name: ui5-tooling-modules-task
16 | afterTask: replaceVersion
17 | configuration:
18 | addToNamespace: true
19 | - name: ui5-tooling-transpile-task
20 | afterTask: replaceVersion
21 | server:
22 | customMiddleware:
23 | - name: ui5-tooling-modules-middleware
24 | afterMiddleware: compression
25 | - name: ui5-tooling-transpile-middleware
26 | afterMiddleware: compression
27 | - name: ui5-middleware-livereload
28 | afterMiddleware: compression
29 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/Component.ts:
--------------------------------------------------------------------------------
1 | import UIComponent from "sap/ui/core/UIComponent";
2 | import models from "./model/models";
3 | import Device from "sap/ui/Device";
4 |
5 | /**
6 | * @namespace com.myorg.myapp
7 | */
8 | export default class Component extends UIComponent {
9 | public static metadata = {
10 | manifest: "json",
11 | };
12 |
13 | private contentDensityClass: string;
14 |
15 | public init(): void {
16 | // call the base component's init function
17 | super.init();
18 |
19 | // create the device model
20 | this.setModel(models.createDeviceModel(), "device");
21 |
22 | // create the views based on the url/hash
23 | this.getRouter().initialize();
24 | }
25 |
26 | /**
27 | * This method can be called to determine whether the sapUiSizeCompact or sapUiSizeCozy
28 | * design mode class should be set, which influences the size appearance of some controls.
29 | * @public
30 | * @returns css class, either 'sapUiSizeCompact' or 'sapUiSizeCozy' - or an empty string if no css class should be set
31 | */
32 | public getContentDensityClass(): string {
33 | if (this.contentDensityClass === undefined) {
34 | // check whether FLP has already set the content density class; do nothing in this case
35 | if (document.body.classList.contains("sapUiSizeCozy") || document.body.classList.contains("sapUiSizeCompact")) {
36 | this.contentDensityClass = "";
37 | } else if (!Device.support.touch) {
38 | // apply "compact" mode if touch is not supported
39 | this.contentDensityClass = "sapUiSizeCompact";
40 | } else {
41 | // "cozy" in case of touch support; default for most sap.m controls, but needed for desktop-first controls like sap.ui.table.Table
42 | this.contentDensityClass = "sapUiSizeCozy";
43 | }
44 | }
45 | return this.contentDensityClass;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/control/WindDirection.gen.d.ts:
--------------------------------------------------------------------------------
1 | import { PropertyBindingInfo } from "sap/ui/base/ManagedObject";
2 | import { $ControlSettings } from "sap/ui/core/Control";
3 |
4 | declare module "./WindDirection" {
5 |
6 | /**
7 | * Interface defining the settings object used in constructor calls
8 | */
9 | interface $WindDirectionSettings extends $ControlSettings {
10 |
11 | /**
12 | * The direction in degrees FROM which the wind blows (this is the internationally common definition). Value 0 means: wind blows from North to South.
13 | */
14 | direction?: number | PropertyBindingInfo | `{${string}}`;
15 | }
16 |
17 | export default interface WindDirection {
18 |
19 | // property: direction
20 |
21 | /**
22 | * Gets current value of property "direction".
23 | *
24 | * The direction in degrees FROM which the wind blows (this is the internationally common definition). Value 0 means: wind blows from North to South.
25 | *
26 | * @returns Value of property "direction"
27 | */
28 | getDirection(): number;
29 |
30 | /**
31 | * Sets a new value for property "direction".
32 | *
33 | * The direction in degrees FROM which the wind blows (this is the internationally common definition). Value 0 means: wind blows from North to South.
34 | *
35 | * When called with a value of "null" or "undefined", the default value of the property will be restored.
36 | *
37 | * @param direction New value for property "direction"
38 | * @returns Reference to "this" in order to allow method chaining
39 | */
40 | setDirection(direction: number): this;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/control/WindDirection.ts:
--------------------------------------------------------------------------------
1 | import Control from "sap/ui/core/Control";
2 | import RenderManager from "sap/ui/core/RenderManager";
3 | import type { MetadataOptions } from "sap/ui/core/Element";
4 |
5 | /**
6 | * @namespace com.myorg.myapp.control
7 | */
8 | export default class WindDirection extends Control {
9 |
10 | // The following three lines were generated and should remain as-is to make TypeScript aware of the constructor signatures
11 | constructor(idOrSettings?: string | $WindDirectionSettings);
12 | constructor(id?: string, settings?: $WindDirectionSettings);
13 | constructor(id?: string, settings?: $WindDirectionSettings) { super(id, settings); }
14 |
15 | static readonly metadata: MetadataOptions = {
16 | properties: {
17 | /**
18 | * The direction in degrees FROM which the wind blows (this is the internationally common definition). Value 0 means: wind blows from North to South.
19 | */
20 | "direction": "float"
21 | }
22 | }
23 |
24 | renderer = {
25 | apiVersion: 2,
26 | render: (rm: RenderManager, control: WindDirection) => {
27 | rm.openStart("div", control);
28 | rm.style("font-size", "2rem");
29 | rm.style("width", "2rem");
30 | rm.style("height", "2rem");
31 | rm.style("display", "inline-block");
32 | rm.style("color", "blue");
33 | rm.style("transform-origin", "center");
34 | rm.style("transform", `rotate(${control.getDirection() + 90}deg)`); // arrow is pointing right by default, direction 0 means blowing FROM the north, so the arrow has to point down
35 | rm.openEnd();
36 | rm.text("➢");
37 | rm.close("div");
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/controller/App.controller.ts:
--------------------------------------------------------------------------------
1 | import BaseController from "./BaseController";
2 |
3 | /**
4 | * @namespace com.myorg.myapp.controller
5 | */
6 | export default class App extends BaseController {
7 | public onInit(): void {
8 | // apply content density mode to root view
9 | this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/controller/BaseController.ts:
--------------------------------------------------------------------------------
1 | import Controller from "sap/ui/core/mvc/Controller";
2 | import UIComponent from "sap/ui/core/UIComponent";
3 | import AppComponent from "../Component";
4 | import Model from "sap/ui/model/Model";
5 | import ResourceModel from "sap/ui/model/resource/ResourceModel";
6 | import ResourceBundle from "sap/base/i18n/ResourceBundle";
7 | import Router from "sap/ui/core/routing/Router";
8 | import History from "sap/ui/core/routing/History";
9 |
10 | /**
11 | * @namespace com.myorg.myapp.controller
12 | */
13 | export default abstract class BaseController extends Controller {
14 | /**
15 | * Convenience method for accessing the component of the controller's view.
16 | * @returns The component of the controller's view
17 | */
18 | public getOwnerComponent(): AppComponent {
19 | return super.getOwnerComponent() as AppComponent;
20 | }
21 |
22 | /**
23 | * Convenience method to get the components' router instance.
24 | * @returns The router instance
25 | */
26 | public getRouter(): Router {
27 | return UIComponent.getRouterFor(this);
28 | }
29 |
30 | /**
31 | * Convenience method for getting the i18n resource bundle of the component.
32 | * @returns The i18n resource bundle of the component
33 | */
34 | public getResourceBundle(): ResourceBundle | Promise {
35 | const oModel = this.getOwnerComponent().getModel("i18n") as ResourceModel;
36 | return oModel.getResourceBundle();
37 | }
38 |
39 | /**
40 | * Convenience method for getting the view model by name in every controller of the application.
41 | * @param [sName] The model name
42 | * @returns The model instance
43 | */
44 | public getModel(sName?: string): Model {
45 | return this.getView().getModel(sName);
46 | }
47 |
48 | /**
49 | * Convenience method for setting the view model in every controller of the application.
50 | * @param oModel The model instance
51 | * @param [sName] The model name
52 | * @returns The current base controller instance
53 | */
54 | public setModel(oModel: Model, sName?: string): BaseController {
55 | this.getView().setModel(oModel, sName);
56 | return this;
57 | }
58 |
59 | /**
60 | * Convenience method for triggering the navigation to a specific target.
61 | * @public
62 | * @param sName Target name
63 | * @param [oParameters] Navigation parameters
64 | * @param [bReplace] Defines if the hash should be replaced (no browser history entry) or set (browser history entry)
65 | */
66 | public navTo(sName: string, oParameters?: object, bReplace?: boolean): void {
67 | this.getRouter().navTo(sName, oParameters, undefined, bReplace);
68 | }
69 |
70 | /**
71 | * Convenience event handler for navigating back.
72 | * It there is a history entry we go one step back in the browser history
73 | * If not, it will replace the current entry of the browser history with the main route.
74 | */
75 | public onNavBack(): void {
76 | const sPreviousHash = History.getInstance().getPreviousHash();
77 | if (sPreviousHash !== undefined) {
78 | window.history.go(-1);
79 | } else {
80 | this.getRouter().navTo("main", {}, undefined, true);
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/controller/Main.controller.ts:
--------------------------------------------------------------------------------
1 | import { InputBase$ChangeEvent } from "sap/m/InputBase";
2 | import BaseController from "./BaseController";
3 | import JSONModel from "sap/ui/model/json/JSONModel";
4 | import * as Nominatim from "nominatim-client";
5 | import MessageBox from "sap/m/MessageBox";
6 |
7 | type WeatherInfo = {
8 | current_weather: {
9 | temperature: number,
10 | windspeed: number,
11 | winddirection: number
12 | },
13 | placeName: string
14 | }
15 |
16 | /**
17 | * @namespace com.myorg.myapp.controller
18 | */
19 | export default class Main extends BaseController {
20 | onInit(): void {
21 | const model = new JSONModel();
22 | this.setModel(model);
23 | void this.loadWeatherData();
24 |
25 | /*
26 | // ALTERNATIVE to declarative event handler attaching in XMLView
27 | const input = this.byId("location");
28 | if (input.isA("sap.m.Input")) { // type guard (unfortunately the control class needs to be given twice)
29 | input.attachChange(function(evt) { // now TS knows input is an Input
30 | const location = evt.getParameter("value"); // type safety even for string-based access
31 | });
32 | }
33 | */
34 | }
35 |
36 | async loadWeatherData(lat = "49.31", lon = "8.64", placeName = "Walldorf") { // default coordinates: Walldorf
37 | const response = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true`);
38 | const jsonData = await response.json() as WeatherInfo;
39 | jsonData.placeName = placeName;
40 | (this.getModel() as JSONModel).setData(jsonData);
41 | }
42 |
43 | locationChange(evt: InputBase$ChangeEvent) {
44 | const location = evt.getParameters().value;
45 |
46 | Nominatim.createClient({
47 | useragent: "UI5 TypeScript Tutorial App", // useragent and referrer required by the terms of use
48 | referer: "https://localhost"
49 | }).search({q: location}).then((results) => {
50 | if (results.length > 0) {
51 | return this.loadWeatherData(results[0].lat, results[0].lon, results[0].display_name); // for simplicity just use the first/best match
52 | } else {
53 | MessageBox.alert(`Location ${location} not found`, {
54 | actions: MessageBox.Action.CLOSE // enums are now properties on the default export!
55 | });
56 | }
57 | }).catch(() => {
58 | MessageBox.alert(`Failure while searching ${location}`, {
59 | actions: MessageBox.Action.CLOSE // enums are now properties on the default export!
60 | });
61 | });
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/i18n/i18n.properties:
--------------------------------------------------------------------------------
1 | appTitle=com.myorg.myapp
2 | appDescription=UI5 Application com.myorg.myapp
3 | btnText=Say Hello
4 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/i18n/i18n_de.properties:
--------------------------------------------------------------------------------
1 | appTitle=com.myorg.myapp
2 | appDescription=UI5 Application com.myorg.myapp
3 | btnText=Sag Hallo
4 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/i18n/i18n_en.properties:
--------------------------------------------------------------------------------
1 | appTitle=com.myorg.myapp
2 | appDescription=UI5 Application com.myorg.myapp
3 | btnText=Say Hello
4 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/index-cdn.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | UI5 Application: com.myorg.myapp
11 |
12 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | UI5 Application: com.myorg.myapp
10 |
11 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "_version": "1.12.0",
3 |
4 | "sap.app": {
5 | "id": "com.myorg.myapp",
6 | "type": "application",
7 | "i18n": "i18n/i18n.properties",
8 | "title": "{{appTitle}}",
9 | "description": "{{appDescription}}",
10 | "applicationVersion": {
11 | "version": "1.0.0"
12 | }
13 | },
14 |
15 | "sap.ui": {
16 | "technology": "UI5",
17 | "icons": {},
18 | "deviceTypes": {
19 | "desktop": true,
20 | "tablet": true,
21 | "phone": true
22 | }
23 | },
24 |
25 | "sap.ui5": {
26 | "rootView": {
27 | "viewName": "com.myorg.myapp.view.App",
28 | "type": "XML",
29 | "async": true,
30 | "id": "app"
31 | },
32 |
33 | "dependencies": {
34 | "minUI5Version": "1.115.1",
35 | "libs": {
36 | "sap.ui.core": {},
37 | "sap.m": {},
38 | "sap.ui.layout": {}
39 | }
40 | },
41 |
42 | "handleValidation": true,
43 |
44 | "contentDensities": {
45 | "compact": true,
46 | "cozy": true
47 | },
48 |
49 | "models": {
50 | "i18n": {
51 | "type": "sap.ui.model.resource.ResourceModel",
52 | "settings": {
53 | "bundleName": "com.myorg.myapp.i18n.i18n"
54 | }
55 | }
56 | },
57 |
58 | "routing": {
59 | "config": {
60 | "routerClass": "sap.m.routing.Router",
61 | "viewType": "XML",
62 | "viewPath": "com.myorg.myapp.view",
63 | "controlId": "app",
64 | "controlAggregation": "pages",
65 | "async": true
66 | },
67 | "routes": [
68 | {
69 | "pattern": "",
70 | "name": "main",
71 | "target": "main"
72 | }
73 | ],
74 | "targets": {
75 | "main": {
76 | "viewId": "main",
77 | "viewName": "Main"
78 | }
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/model/formatter.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | formatValue: (value: string) => {
3 | return value?.toUpperCase();
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/model/models.ts:
--------------------------------------------------------------------------------
1 | import JSONModel from "sap/ui/model/json/JSONModel";
2 | import BindingMode from "sap/ui/model/BindingMode";
3 |
4 | import Device from "sap/ui/Device";
5 |
6 |
7 | export default {
8 | createDeviceModel: () => {
9 | const oModel = new JSONModel(Device);
10 | oModel.setDefaultBindingMode(BindingMode.OneWay);
11 | return oModel;
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/test/Test.qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/test/integration/HelloJourney.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-floating-promises */
2 | import opaTest from "sap/ui/test/opaQunit";
3 | import MainPage from "./pages/MainPage";
4 |
5 | const onTheMainPage = new MainPage();
6 |
7 | QUnit.module("Sample Hello Journey");
8 |
9 | opaTest("Should show location Heidelberg", function () {
10 | // Arrangements
11 | onTheMainPage.iStartMyUIComponent({
12 | componentConfig: {
13 | name: "com.myorg.myapp"
14 | }
15 | });
16 |
17 | // Actions
18 | onTheMainPage.iEnterLocationHeidelberg();
19 |
20 | // Assertions
21 | onTheMainPage.iShouldSeeTheLocationHeidelberg();
22 |
23 | // Cleanup
24 | onTheMainPage.iTeardownMyApp();
25 | });
26 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/test/integration/opaTests.qunit.ts:
--------------------------------------------------------------------------------
1 | import "./HelloJourney";
2 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/test/integration/pages/MainPage.ts:
--------------------------------------------------------------------------------
1 | import Text from "sap/m/Text";
2 | import UI5Element from "sap/ui/core/Element";
3 | import Opa5 from "sap/ui/test/Opa5";
4 | import EnterText from "sap/ui/test/actions/EnterText";
5 |
6 | const viewName = "com.myorg.myapp.view.Main";
7 |
8 | export default class MainPage extends Opa5 {
9 | // Actions
10 | iEnterLocationHeidelberg() {
11 | this.waitFor({
12 | id: "location",
13 | viewName,
14 | actions: new EnterText({
15 | text: "Heidelberg"
16 | }),
17 | errorMessage: "Did not find the 'location' input on the Main view"
18 | });
19 | }
20 |
21 | // Assertions
22 | iShouldSeeTheLocationHeidelberg() {
23 | this.waitFor({
24 | controlType: "sap.m.Text",
25 | viewName,
26 | check: function(text: UI5Element[]): boolean {
27 | return ( text[0]).getText(false).includes("Heidelberg");
28 | },
29 | success: function () {
30 | Opa5.assert.ok(true, "The location Heidelberg is displayed");
31 | },
32 | errorMessage: "Did not find the text control"
33 | });
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/test/testsuite.qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/test/testsuite.qunit.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "Unit test suite for the UI5 Application: com.myorg.myapp",
3 | defaults: {
4 | page: "ui5://test-resources/com/myorg/myapp/Test.qunit.html?testsuite={suite}&test={name}",
5 | qunit: {
6 | version: 2
7 | },
8 | ui5: {
9 | theme: "sap_horizon"
10 | },
11 | loader: {
12 | paths: {
13 | "com/myorg/myapp": "../"
14 | }
15 | }
16 | },
17 | tests: {
18 | "unit/unitTests": {
19 | title: "Unit tests for the UI5 Application: com.myorg.myapp"
20 | },
21 | "integration/opaTests": {
22 | title: "Integration tests for the UI5 Application: com.myorg.myapp"
23 | }
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/test/unit/control/WindDirection.qunit.ts:
--------------------------------------------------------------------------------
1 | import WindDirection from "com/myorg/myapp/control/WindDirection";
2 |
3 | // prepare DOM (create a location to place the custom control into)
4 | const elem = document.createElement("div");
5 | elem.id = "uiArea1";
6 | document.body.appendChild(elem);
7 |
8 | QUnit.module("Basic Checks");
9 |
10 | // some basic properties checks
11 | QUnit.test("Properties", function (assert) {
12 | assert.expect(1);
13 | const oWindDirection = new WindDirection({
14 | direction: 25
15 | });
16 | assert.equal(oWindDirection.getDirection(), 25, "Check direction equals 25");
17 | });
18 |
19 | // some basic rendering checks
20 | QUnit.test("Rendering", function (assert) {
21 | const done = assert.async();
22 | assert.expect(1);
23 | const oWindDirection = new WindDirection({
24 | direction: 25
25 | });
26 | oWindDirection.addEventDelegate({
27 | onAfterRendering: () => {
28 | const transform = (oWindDirection.getDomRef() as HTMLElement).style.transform;
29 | assert.strictEqual(transform, `rotate(${oWindDirection.getDirection() + 90}deg)`, 'Transform is in sync with property');
30 | done();
31 | }
32 | });
33 | oWindDirection.placeAt("uiArea1");
34 | });
35 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/test/unit/controller/Main.qunit.ts:
--------------------------------------------------------------------------------
1 | import Main from "com/myorg/myapp/controller/Main.controller";
2 |
3 | QUnit.module("Sample Main controller test");
4 |
5 | QUnit.test("The Main controller class has all custom methods", function (assert) {
6 | assert.expect(2);
7 | assert.strictEqual(typeof Main.prototype.locationChange, "function");
8 | assert.strictEqual(typeof Main.prototype.loadWeatherData, "function");
9 | });
10 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/test/unit/unitTests.qunit.ts:
--------------------------------------------------------------------------------
1 | import "./controller/Main.qunit";
2 | import "./control/WindDirection.qunit";
3 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/view/App.view.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/exercises/ex6/com.myorg.myapp/webapp/view/Main.view.xml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/generator/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | # We recommend you to keep these unchanged
9 | max_line_length = 200
10 | end_of_line = lf
11 | charset = utf-8
12 | trim_trailing_whitespace = true
13 | insert_final_newline = true
14 |
15 | # Change these settings to your own preference
16 | indent_style = tab
17 | indent_size = 2
18 |
19 | [*.{yaml,yml}]
20 | indent_style = space
21 |
22 | [*.md]
23 | indent_style = unset
24 | trim_trailing_whitespace = false
25 |
--------------------------------------------------------------------------------
/generator/.eslintignore:
--------------------------------------------------------------------------------
1 | # Ignore node_files
2 | node_modules/
3 |
4 | # Ignore templates
5 | generators/*/templates/
6 |
7 | # Ignore test files
8 | test/
9 |
--------------------------------------------------------------------------------
/generator/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "env": {
4 | "node": true,
5 | "es6": true
6 | },
7 | "parserOptions": {
8 | "ecmaVersion": 2022,
9 | "sourceType": "module"
10 | },
11 | "extends": ["eslint:recommended", "plugin:jsdoc/recommended"],
12 | "plugins": ["jsdoc"]
13 | }
14 |
--------------------------------------------------------------------------------
/generator/.gitignore:
--------------------------------------------------------------------------------
1 | # Hidden Files
2 | _
3 | test/*/
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 |
12 | # Dependency directories
13 | node_modules/
14 |
15 | .DS_Store
16 |
--------------------------------------------------------------------------------
/generator/.lintstagedrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "**/*.js": "eslint"
3 | }
4 |
--------------------------------------------------------------------------------
/generator/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore test files
2 | test/*/
3 |
4 | # Ignore all source files using the placeholders to kill the syntax
5 | generators/app/templates/webapp/Component.ts
6 | generators/app/templates/webapp/model/models.ts
7 | generators/app/templates/webapp/view/*.xml
8 |
--------------------------------------------------------------------------------
/generator/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": false,
3 | "trailingComma": "none",
4 | "overrides": [
5 | {
6 | "files": ["*.xml"],
7 | "options": {
8 | "xmlWhitespaceSensitivity": "ignore"
9 | }
10 | },
11 | {
12 | "files": ["*.properties"],
13 | "options": {
14 | "keySeparator": "="
15 | }
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/generator/README.md:
--------------------------------------------------------------------------------
1 | # generator-ui5-ts-app
2 |
3 | [![License Status][license-image]][license-url]
4 |
5 | [Yeoman](https://yeoman.io/) generator kickstarting the development of UI5 applications using TypeScript. The generator incorporates the latest best-practices, is using the [UI5 Tooling](https://sap.github.io/ui5-tooling/) and the UI5 Tooling extensions provided by the [UI5 community](https://github.com/ui5-community/ui5-ecosystem-showcase/). It is maintained by the UI5 community and [OpenUI5](https://openui5.org)/[SAPUI5](https://ui5.sap.com) developers. This generator was build as a plug-in for the community project [Easy-UI5](https://github.com/SAP/generator-easy-ui5/) by [SAP](https://github.com/SAP/).
6 |
7 | ## Usage with Easy UI5
8 |
9 | ```bash
10 | $> npm i -g yo
11 | $> yo easy-ui5 ts-app
12 |
13 | _-----_
14 | | | ╭──────────────────────────╮
15 | |--(o)--| │ Welcome to the easy-ui5 │
16 | `---------´ │ generator! │
17 | ( _´U`_ ) ╰──────────────────────────╯
18 | /___A___\ /
19 | | ~ |
20 | __'.___.'__
21 | ´ ` |° ´ Y `
22 | ```
23 |
24 | After the generation of your project you can use `npm start` (or `yarn start`) to start the local development server.
25 |
26 | ## Standalone usage
27 |
28 | Note the different greeting when the generator starts:
29 |
30 | ```bash
31 | $> npm i -g yo
32 | $> yo ./generator-ui5-ts-app
33 |
34 | _-----_ ╭──────────────────────────╮
35 | | | │ Welcome to the │
36 | |--(o)--| │ generator-ui5-ts-app │
37 | `---------´ │ generator! │
38 | ( _´U`_ ) ╰──────────────────────────╯
39 | /___A___\ /
40 | | ~ |
41 | __'.___.'__
42 | ´ ` |° ´ Y `
43 | ```
44 |
45 | ## Support
46 |
47 | Please use the GitHub bug tracking system to post questions, bug reports or to create pull requests.
48 |
49 | ## Contributing
50 |
51 | We welcome any type of contribution (code contributions, pull requests, issues) to this generator equally.
52 |
53 | ## License
54 |
55 | This project is licensed under the Apache Software License, version 2.0 except as noted otherwise in the LICENSE file.
56 |
57 | [license-image]: https://img.shields.io/github/license/ui5-community/generator-ui5-ts-app.svg
58 | [license-url]: https://github.com/ui5-community/generator-ui5-ts-app/blob/main/LICENSE
59 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/_.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | # We recommend you to keep these unchanged
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 |
14 | # Change these settings to your own preference
15 | indent_style = tab
16 | indent_size = 2
17 |
18 | [*.{yaml,yml}]
19 | indent_style = space
20 |
21 | [*.md]
22 | indent_style = unset
23 | trim_trailing_whitespace = false
24 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/_.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "root": true,
3 | "env": {
4 | "browser": true,
5 | "es6": true,
6 | "node": true
7 | },
8 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking"],
9 | "parser": "@typescript-eslint/parser",
10 | "parserOptions": {
11 | "project": [`${__dirname}/tsconfig.json`],
12 | "sourceType": "module"
13 | },
14 | "plugins": ["@typescript-eslint"],
15 | "ignorePatterns": [".eslintrc.js"]
16 | };
17 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/_.gitignore:
--------------------------------------------------------------------------------
1 | # build results
2 | dist
3 | coverage
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 |
12 | # Dependency directories
13 | node_modules/
14 |
15 | .DS_Store
16 | .env
17 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/karma-ci-cov.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | require("./karma-ci.conf")(config);
3 | config.set({
4 | reporters: ["progress", "coverage"],
5 | preprocessors: {
6 | "webapp/**/*.ts": ["ui5-transpile"]
7 | },
8 | coverageReporter: {
9 | dir: "coverage",
10 | reporters: [
11 | { type: "html", subdir: "report-html" },
12 | { type: "cobertura", subdir: ".", file: "cobertura.txt" },
13 | { type: "lcovonly", subdir: ".", file: "report-lcovonly.txt" },
14 | { type: "text-summary" }
15 | ]
16 | }
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/karma-ci.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | require("./karma.conf")(config);
3 | config.set({
4 | browsers: ["ChromeHeadless"],
5 | singleRun: true
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/karma.conf.js:
--------------------------------------------------------------------------------
1 | // karma-ui5 usage: https://github.com/SAP/karma-ui5
2 | module.exports = function (config) {
3 | config.set({
4 | frameworks: ["ui5"],
5 | browsers: ["Chrome"]
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "<%= namespace %>",
3 | "version": "1.0.0",
4 | "description": "UI5 Application: <%= namespace %>",
5 | "author": "<%= author %>",
6 | "license": "Apache-2.0",
7 | "scripts": {
8 | "clean": "rimraf dist coverage",
9 | "build": "ui5 build --clean-dest",
10 | "build:opt": "ui5 build self-contained --clean-dest --all",
11 | "start": "ui5 serve --port 8080 -o index.html",
12 | "start-cdn": "ui5 serve --port 8080 -o index-cdn.html",
13 | "start:dist": "npm start -- --config ui5-dist.yaml",
14 | "start:dist-cdn": "npm run start-cdn -- --config ui5-dist.yaml",
15 | "ts-typecheck": "tsc --noEmit",
16 | "lint": "eslint webapp",
17 | "karma": "karma start",
18 | "karma-ci": "karma start karma-ci.conf.js",
19 | "karma-ci-cov": "karma start karma-ci-cov.conf.js",
20 | "test": "npm run lint && npm run karma-ci-cov"
21 | },
22 | "devDependencies": {
23 | "<%= tstypes %>": "<%= tstypesVersion %>",
24 | "@typescript-eslint/eslint-plugin": "^5.61.0",
25 | "@typescript-eslint/parser": "^5.61.0",
26 | "@ui5/cli": "^4.0.11",
27 | "eslint": "^8.44.0",
28 | "karma": "^6.4.2",
29 | "karma-chrome-launcher": "^3.2.0",
30 | "karma-coverage": "^2.2.1",
31 | "karma-ui5": "^3.0.3",
32 | "karma-ui5-transpile": "^0.3.22",
33 | "rimraf": "^5.0.1",
34 | "typescript": "^5.1.6",
35 | "ui5-middleware-livereload": "^0.8.4",
36 | "ui5-tooling-transpile": "^0.7.17"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2022",
4 | "module": "es2022",
5 | "moduleResolution": "node",
6 | "skipLibCheck": true,
7 | "allowJs": true,
8 | "strict": true,
9 | "strictNullChecks": false,
10 | "strictPropertyInitialization": false,
11 | "rootDir": "./webapp",
12 | "types": ["<%= tstypes %>", "@types/qunit"],
13 | "paths": {
14 | "<%= appURI %>/*": ["./webapp/*"],
15 | "unit/*": ["./webapp/test/unit/*"],
16 | "integration/*": ["./webapp/test/integration/*"]
17 | }
18 | },
19 | "include": ["./webapp/**/*"],
20 | "exclude": ["./webapp/coverage/**/*"]
21 | }
22 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/ui5-dist.yaml:
--------------------------------------------------------------------------------
1 | specVersion: "3.0"
2 | metadata:
3 | name: <%= appId %>
4 | type: application
5 | resources:
6 | configuration:
7 | paths:
8 | webapp: dist
9 | framework:
10 | name: <%= framework %>
11 | version: "<%= frameworkVersion %>"
12 | libraries:
13 | - name: sap.m
14 | - name: sap.ui.core
15 | - name: themelib_<%= defaultTheme %>
16 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/ui5.yaml:
--------------------------------------------------------------------------------
1 | specVersion: "3.0"
2 | metadata:
3 | name: <%= appId %>
4 | type: application
5 | framework:
6 | name: <%= framework %>
7 | version: "<%= frameworkVersion %>"
8 | libraries:
9 | - name: sap.m
10 | - name: sap.ui.core
11 | - name: themelib_<%= defaultTheme %>
12 | builder:
13 | customTasks:
14 | - name: ui5-tooling-transpile-task
15 | afterTask: replaceVersion
16 | server:
17 | customMiddleware:
18 | - name: ui5-tooling-transpile-middleware
19 | afterMiddleware: compression
20 | - name: ui5-middleware-livereload
21 | afterMiddleware: compression
22 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/Component.ts:
--------------------------------------------------------------------------------
1 | import UIComponent from "sap/ui/core/UIComponent";
2 | import models from "./model/models";<% if (gte11150) { %>
3 | import Device from "sap/ui/Device";<% } else { %>
4 | import * as Device from "sap/ui/Device"; // for UI5 >= 1.115.0 use: import Device from "sap/ui/Device";<% } %>
5 |
6 | /**
7 | * @namespace <%= appId %>
8 | */
9 | export default class Component extends UIComponent {
10 | public static metadata = {
11 | manifest: "json",
12 | };
13 |
14 | private contentDensityClass: string;
15 |
16 | public init(): void {
17 | // call the base component's init function
18 | super.init();
19 |
20 | // create the device model
21 | this.setModel(models.createDeviceModel(), "device");
22 |
23 | // create the views based on the url/hash
24 | this.getRouter().initialize();
25 | }
26 |
27 | /**
28 | * This method can be called to determine whether the sapUiSizeCompact or sapUiSizeCozy
29 | * design mode class should be set, which influences the size appearance of some controls.
30 | * @public
31 | * @returns css class, either 'sapUiSizeCompact' or 'sapUiSizeCozy' - or an empty string if no css class should be set
32 | */
33 | public getContentDensityClass(): string {
34 | if (this.contentDensityClass === undefined) {
35 | // check whether FLP has already set the content density class; do nothing in this case
36 | if (document.body.classList.contains("sapUiSizeCozy") || document.body.classList.contains("sapUiSizeCompact")) {
37 | this.contentDensityClass = "";
38 | } else if (!Device.support.touch) {
39 | // apply "compact" mode if touch is not supported
40 | this.contentDensityClass = "sapUiSizeCompact";
41 | } else {
42 | // "cozy" in case of touch support; default for most sap.m controls, but needed for desktop-first controls like sap.ui.table.Table
43 | this.contentDensityClass = "sapUiSizeCozy";
44 | }
45 | }
46 | return this.contentDensityClass;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/controller/App.controller.ts:
--------------------------------------------------------------------------------
1 | import BaseController from "./BaseController";
2 |
3 | /**
4 | * @namespace <%= appId %>.controller
5 | */
6 | export default class App extends BaseController {
7 | public onInit(): void {
8 | // apply content density mode to root view
9 | this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/controller/BaseController.ts:
--------------------------------------------------------------------------------
1 | import Controller from "sap/ui/core/mvc/Controller";
2 | import UIComponent from "sap/ui/core/UIComponent";
3 | import AppComponent from "../Component";
4 | import Model from "sap/ui/model/Model";
5 | import ResourceModel from "sap/ui/model/resource/ResourceModel";
6 | import ResourceBundle from "sap/base/i18n/ResourceBundle";
7 | import Router from "sap/ui/core/routing/Router";
8 | import History from "sap/ui/core/routing/History";
9 |
10 | /**
11 | * @namespace <%= appId %>.controller
12 | */
13 | export default abstract class BaseController extends Controller {
14 | /**
15 | * Convenience method for accessing the component of the controller's view.
16 | * @returns The component of the controller's view
17 | */
18 | public getOwnerComponent(): AppComponent {
19 | return super.getOwnerComponent() as AppComponent;
20 | }
21 |
22 | /**
23 | * Convenience method to get the components' router instance.
24 | * @returns The router instance
25 | */
26 | public getRouter(): Router {
27 | return UIComponent.getRouterFor(this);
28 | }
29 |
30 | /**
31 | * Convenience method for getting the i18n resource bundle of the component.
32 | * @returns The i18n resource bundle of the component
33 | */
34 | public getResourceBundle(): ResourceBundle | Promise {
35 | const oModel = this.getOwnerComponent().getModel("i18n") as ResourceModel;
36 | return oModel.getResourceBundle();
37 | }
38 |
39 | /**
40 | * Convenience method for getting the view model by name in every controller of the application.
41 | * @param [sName] The model name
42 | * @returns The model instance
43 | */
44 | public getModel(sName?: string): Model {
45 | return this.getView().getModel(sName);
46 | }
47 |
48 | /**
49 | * Convenience method for setting the view model in every controller of the application.
50 | * @param oModel The model instance
51 | * @param [sName] The model name
52 | * @returns The current base controller instance
53 | */
54 | public setModel(oModel: Model, sName?: string): BaseController {
55 | this.getView().setModel(oModel, sName);
56 | return this;
57 | }
58 |
59 | /**
60 | * Convenience method for triggering the navigation to a specific target.
61 | * @public
62 | * @param sName Target name
63 | * @param [oParameters] Navigation parameters
64 | * @param [bReplace] Defines if the hash should be replaced (no browser history entry) or set (browser history entry)
65 | */
66 | public navTo(sName: string, oParameters?: object, bReplace?: boolean): void {
67 | this.getRouter().navTo(sName, oParameters, undefined, bReplace);
68 | }
69 |
70 | /**
71 | * Convenience event handler for navigating back.
72 | * It there is a history entry we go one step back in the browser history
73 | * If not, it will replace the current entry of the browser history with the main route.
74 | */
75 | public onNavBack(): void {
76 | const sPreviousHash = History.getInstance().getPreviousHash();
77 | if (sPreviousHash !== undefined) {
78 | window.history.go(-1);
79 | } else {
80 | this.getRouter().navTo("main", {}, undefined, true);
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/controller/Main.controller.ts:
--------------------------------------------------------------------------------
1 | import MessageBox from "sap/m/MessageBox";
2 | import BaseController from "./BaseController";
3 |
4 | /**
5 | * @namespace <%= appId %>.controller
6 | */
7 | export default class Main extends BaseController {
8 | public sayHello(): void {
9 | MessageBox.show("Hello World!");
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/i18n/i18n.properties:
--------------------------------------------------------------------------------
1 | appTitle=<%= namespace %>
2 | appDescription=UI5 Application <%= namespace %>
3 | btnText=Say Hello
4 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/i18n/i18n_de.properties:
--------------------------------------------------------------------------------
1 | appTitle=<%= namespace %>
2 | appDescription=UI5 Application <%= namespace %>
3 | btnText=Sag Hallo
4 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/i18n/i18n_en.properties:
--------------------------------------------------------------------------------
1 | appTitle=<%= namespace %>
2 | appDescription=UI5 Application <%= namespace %>
3 | btnText=Say Hello
4 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/index-cdn.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | UI5 Application: <%= namespace %>
11 |
12 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | UI5 Application: <%= namespace %>
10 |
11 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "_version": "1.12.0",
3 |
4 | "sap.app": {
5 | "id": "<%= appId %>",
6 | "type": "application",
7 | "i18n": "i18n/i18n.properties",
8 | "title": "{{appTitle}}",
9 | "description": "{{appDescription}}",
10 | "applicationVersion": {
11 | "version": "1.0.0"
12 | }
13 | },
14 |
15 | "sap.ui": {
16 | "technology": "UI5",
17 | "icons": {},
18 | "deviceTypes": {
19 | "desktop": true,
20 | "tablet": true,
21 | "phone": true
22 | }
23 | },
24 |
25 | "sap.ui5": {
26 | "rootView": {
27 | "viewName": "<%= appId %>.view.App",
28 | "type": "XML",
29 | "async": true,
30 | "id": "app"
31 | },
32 |
33 | "dependencies": {
34 | "minUI5Version": "<%= frameworkVersion %>",
35 | "libs": {
36 | "sap.ui.core": {},
37 | "sap.m": {}
38 | }
39 | },
40 |
41 | "handleValidation": true,
42 |
43 | "contentDensities": {
44 | "compact": true,
45 | "cozy": true
46 | },
47 |
48 | "models": {
49 | "i18n": {
50 | "type": "sap.ui.model.resource.ResourceModel",
51 | "settings": {
52 | "bundleName": "<%= appId %>.i18n.i18n"
53 | }
54 | }
55 | },
56 |
57 | "routing": {
58 | "config": {
59 | "routerClass": "sap.m.routing.Router",
60 | "viewType": "XML",
61 | "viewPath": "<%= appId %>.view",
62 | "controlId": "app",
63 | "controlAggregation": "pages",
64 | "async": true
65 | },
66 | "routes": [
67 | {
68 | "pattern": "",
69 | "name": "main",
70 | "target": "main"
71 | }
72 | ],
73 | "targets": {
74 | "main": {
75 | "viewId": "main",
76 | "viewName": "Main"
77 | }
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/model/formatter.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | formatValue: (value: string) => {
3 | return value?.toUpperCase();
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/model/models.ts:
--------------------------------------------------------------------------------
1 | import JSONModel from "sap/ui/model/json/JSONModel";
2 | import BindingMode from "sap/ui/model/BindingMode";
3 | <% if (gte11150) { %>
4 | import Device from "sap/ui/Device";
5 | <% } else { %>
6 | import * as Device from "sap/ui/Device"; // for UI5 >= 1.115.0 use: import Device from "sap/ui/Device";
7 | <% } %>
8 |
9 | export default {
10 | createDeviceModel: () => {
11 | const oModel = new JSONModel(Device);
12 | oModel.setDefaultBindingMode(BindingMode.OneWay);
13 | return oModel;
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/test/Test.qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/test/integration/HelloJourney.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-floating-promises */
2 | import opaTest from "sap/ui/test/opaQunit";
3 | import MainPage from "./pages/MainPage";
4 |
5 | const onTheMainPage = new MainPage();
6 |
7 | QUnit.module("Sample Hello Journey");
8 |
9 | opaTest("Should open the Hello dialog", function () {
10 | // Arrangements
11 | onTheMainPage.iStartMyUIComponent({
12 | componentConfig: {
13 | name: "<%= appId %>"
14 | }
15 | });
16 |
17 | // Actions
18 | onTheMainPage.iPressTheSayHelloWithDialogButton();
19 |
20 | // Assertions
21 | onTheMainPage.iShouldSeeTheHelloDialog();
22 |
23 | // Actions
24 | onTheMainPage.iPressTheOkButtonInTheDialog();
25 |
26 | // Assertions
27 | onTheMainPage.iShouldNotSeeTheHelloDialog();
28 |
29 | // Cleanup
30 | onTheMainPage.iTeardownMyApp();
31 | });
32 |
33 | opaTest("Should close the Hello dialog", function () {
34 | // Arrangements
35 | onTheMainPage.iStartMyUIComponent({
36 | componentConfig: {
37 | name: "<%= appId %>"
38 | }
39 | });
40 |
41 | // Actions
42 | onTheMainPage.iPressTheSayHelloWithDialogButton();
43 | onTheMainPage.iPressTheOkButtonInTheDialog();
44 |
45 | // Assertions
46 | onTheMainPage.iShouldNotSeeTheHelloDialog();
47 |
48 | // Cleanup
49 | onTheMainPage.iTeardownMyApp();
50 | });
51 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/test/integration/opaTests.qunit.ts:
--------------------------------------------------------------------------------
1 | import "./HelloJourney";
2 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/test/integration/pages/MainPage.ts:
--------------------------------------------------------------------------------
1 | import Opa5 from "sap/ui/test/Opa5";
2 | import Press from "sap/ui/test/actions/Press";
3 |
4 | const viewName = "<%= appId %>.view.Main";
5 |
6 | export default class MainPage extends Opa5 {
7 | // Actions
8 | iPressTheSayHelloWithDialogButton() {
9 | this.waitFor({
10 | id: "helloButton",
11 | viewName,
12 | actions: new Press(),
13 | errorMessage: "Did not find the 'Say Hello With Dialog' button on the Main view"
14 | });
15 | }
16 |
17 | iPressTheOkButtonInTheDialog() {
18 | this.waitFor({
19 | controlType: "sap.m.Button",
20 | searchOpenDialogs: true,
21 | viewName,
22 | actions: new Press(),
23 | errorMessage: "Did not find the 'OK' button in the Dialog"
24 | });
25 | }
26 |
27 | // Assertions
28 | iShouldSeeTheHelloDialog() {
29 | this.waitFor({
30 | controlType: "sap.m.Dialog",
31 | success: function () {
32 | // we set the view busy, so we need to query the parent of the app
33 | Opa5.assert.ok(true, "The dialog is open");
34 | },
35 | errorMessage: "Did not find the dialog control"
36 | });
37 | }
38 |
39 | iShouldNotSeeTheHelloDialog() {
40 | this.waitFor({
41 | controlType: "sap.m.App", // dummy, I just want a check function, where I can search the DOM. Probably there is a better way for a NEGATIVE test (NO dialog).
42 | check: function () {
43 | return document.querySelectorAll(".sapMDialog").length === 0;
44 | },
45 | success: function () {
46 | Opa5.assert.ok(true, "No dialog is open");
47 | }
48 | });
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/test/testsuite.qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/test/testsuite.qunit.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | name: "Unit test suite for the UI5 Application: <%= appId %>",
3 | defaults: {
4 | page: "ui5://test-resources/<%= appURI %>/Test.qunit.html?testsuite={suite}&test={name}",
5 | qunit: {
6 | version: 2
7 | },
8 | ui5: {
9 | theme: "<%= defaultTheme %>"
10 | },
11 | loader: {
12 | paths: {
13 | "<%= appURI %>": "../"
14 | }
15 | }
16 | },
17 | tests: {
18 | "unit/unitTests": {
19 | title: "Unit tests for the UI5 Application: <%= appId %>"
20 | },
21 | "integration/opaTests": {
22 | title: "Integration tests for the UI5 Application: <%= appId %>"
23 | }
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/test/unit/controller/Main.qunit.ts:
--------------------------------------------------------------------------------
1 | import Main from "<%= appURI %>/controller/Main.controller";
2 |
3 | QUnit.module("Sample Main controller test");
4 |
5 | QUnit.test("The Main controller class has a sayHello method", function (assert) {
6 | // as a very basic test example just check the presence of the "sayHello" method
7 | assert.strictEqual(typeof Main.prototype.sayHello, "function");
8 | });
9 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/test/unit/unitTests.qunit.ts:
--------------------------------------------------------------------------------
1 | import "./controller/Main.qunit";
2 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/view/App.view.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/generator/generators/app/templates/webapp/view/Main.view.xml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
17 |
18 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/generator/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "generator-ui5-ts-app",
3 | "version": "1.1.0",
4 | "description": "Yeoman-based (sub-)generator for a basic UI5 application based on TypeScript",
5 | "main": "generators/app/index.js",
6 | "type": "module",
7 | "scripts": {
8 | "lint": "eslint .",
9 | "lint:fix": "eslint . --fix",
10 | "lint:staged": "lint-staged",
11 | "format": "prettier --write .",
12 | "format:staged": "pretty-quick --staged --verbose",
13 | "test": "cd test && yo ../../generator-ui5-ts-app",
14 | "prepare": "node ./.husky/skip.js || husky install",
15 | "hooks:pre-commit": "npm-run-all --sequential format:staged lint:staged"
16 | },
17 | "keywords": [
18 | "yeoman-generator",
19 | "ui5",
20 | "openui5",
21 | "sapui5",
22 | "application",
23 | "typescript"
24 | ],
25 | "author": "Peter Muessig",
26 | "license": "Apache-2.0",
27 | "repository": {
28 | "type": "git",
29 | "url": "https://github.com/ui5-community/generator-ui5-ts-app.git"
30 | },
31 | "bugs": {
32 | "url": "https://github.com/ui5-community/generator-ui5-ts-app/issues"
33 | },
34 | "homepage": "https://github.com/ui5-community/generator-ui5-ts-app#readme",
35 | "dependencies": {
36 | "chalk": "^5.2.0",
37 | "glob": "^10.3.0",
38 | "package-json": "^8.1.1",
39 | "semver": "^7.5.2",
40 | "upath": "^2.0.1",
41 | "yeoman-generator": "^5.9.0",
42 | "yosay": "^2.0.2"
43 | },
44 | "devDependencies": {
45 | "@prettier/plugin-xml": "^2.2.0",
46 | "eslint": "^8.43.0",
47 | "eslint-plugin-jsdoc": "^46.2.6",
48 | "husky": "^8.0.3",
49 | "lint-staged": "^13.2.2",
50 | "npm-run-all": "^4.1.5",
51 | "prettier": "^2.8.8",
52 | "prettier-plugin-properties": "^0.2.0",
53 | "pretty-quick": "^3.1.3",
54 | "yeoman-assert": "^3.1.1",
55 | "yeoman-test": "^7.4.0"
56 | },
57 | "overrides": {
58 | "minimist": ">=1.2.6"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------