In this exercise, you will load weather data and display it. And learn a lot about how TypeScript helps with writing code.
After completing this section, you will have a basic understanding of how UI5 application code looks different due to TypeScript and due to the modern JavaScript syntax which we suggest to use in TypeScript applications.
-
Open the file
webapp/Component.ts
in a TypeScript-aware code editor of your choice (e.g. Visual Studio Code).Now look at the overall file structure. It starts with
import
statements and a class definition:import UIComponent from "sap/ui/core/UIComponent"; import models from "./model/models"; import Device from "sap/ui/Device"; /** * @namespace com.myorg.myapp */ export default class Component extends UIComponent { ...
Both do not have anything to do with TypeScript! This is just modern JavaScript syntax for loading dependencies and for defining classes.
But this syntax is still important to understand for writing UI5 apps in TypeScript: Because TypeScript code anyway needs to be transpiled before being executed (browsers cannot execute TypeScript, only JavaScript!), UI5 decided to add another conversion step during transpilation. This step converts this modern syntax to traditional UI5 dependency loading (with
sap.ui.require(...)
orsap.ui.define(...)
) and class definition (withUIComponent.extend(...)
).The
@namespace
JSDoc comment above the class definition is required for enabling this conversion to construct the full name of the class in UI5 terms.How member methods and properties like the metadata are written inside the class body is also standard JavaScript class syntax.
-
Actual TypeScript syntax only has extremely little footprint in this file - everything else is pure JavaScript:
- The "string" type for the private member variable
private contentDensityClass : string;
- The return types of the two member methods, e.g. "string" for the
public getContentDensityClass() : string {...
method
- The "string" type for the private member variable
-
The first benefits of TypeScript can be seen when you hover the mouse pointer above class names like
UIComponent
or called methods likesuper.init()
: the editor displays the respective API documentation in-place. -
Depending on the used code editor, you can also navigate to the definition of the respective UI5 API. E.g. in VSCode by holding the
Ctrl
key while clicking the name (Cmd
+click on MacOS). This does not lead to the original source code of UI5, which is written in JavaScript, but to the TypeScript type definition files of UI5. Nevertheless it can be very helpful to browse through the methods offered by a class or to navigate further up the inheritance chain. -
There are further benefits of TypeScript, e.g. providing code completion and development-time error checks. You will learn about them in subsequent steps.
After completing this step, a model for storing weather data will be created. This is the first step where you actually write code. It will hence provide another glimpse of the benefits TypeScript provides.
⚠️ Remark: whenever you completed a step in this tutorial and the result does not show up in the browser as expected, make sure you saved all changes! This sounds trivial, but is actually the single most frequent cause of issues.
-
In the file
webapp/controller/Main.controller.ts
, delete thepublic sayHello() ...
method and at its previous location instead create theonInit
method to execute code when the controller is instantiated.In the end, the method should look like this:
onInit(): void { const model = new JSONModel(); this.setModel(model); }
But to experience how code assist works and makes development more efficient, we recommend that you just start typing "ini" manually and keep an eye on the suggestions provided by your editor!
The editor knows that all Controllers have an
onInit
method and not only suggests to add it, but also shows the according documentation right away. You will not accidentally name the methodinit()
anymore and you don't need to type all the braces - just accept the suggestion and you get the method body for free:onInit(): void { }
-
Now continue implementing the method by creating a model and setting it on the view. Start typing
const model = new JS
and again there is a helpful suggestion: even though the classJSONModel
is not yet available within the file, TypeScript knows that it exists and suggests it, as soon as you typednew JS
. Selecting it from the list will not only complete the written code, but also add the requiredimport
statement at the top of the file.If you just type
JSONModel
without selecting anything from the list, the import may not be created. Instead, an error will notify you that the class is not known.You could then manually type the import, but at least in Visual Studio Code the error popup does also offer a "Quick Fix..." option, which upon selection suggests adding the missing import.
To complete this step of the exercise, you have to add the import using any of the above ways.
Then, set the created model on the View. Similarly, as you type
this.setM
, the editor will suggest thesetModel
method.As mentioned, in the end, the method should look like this:
onInit(): void { const model = new JSONModel(); this.setModel(model); }
Keep these features in mind as you write more TypeScript code in the subsequent exercises, as they help you proceeding faster and are some of the main reasons for using TypeScript.
-
We now need to load the data. To do so, after the end of the
onInit
method body, add this method:async loadWeatherData(lat = "49.31", lon = "8.64") { // default coordinates: Walldorf const response = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true`); const jsonData = await response.json(); this.getModel().setData(jsonData); }
Ignore the displayed error for the time being - we'll deal with it later. TypeScript transpiles the code despite this error.
While some parts might look unfamiliar, this is all pure JavaScript code without any type information or TypeScript-specific syntax. It uses some relatively modern JavaScript constructs like an
async
/await
, default parameters (default values for thelat
/lon
method parameters), thefetch
API for loading remote data and a template string for easier string concatenation.
But essentially it simply loads weather data for the given coordinates in JSON format and puts it into the model.Although you have written zero type information, the TypeScript compiler uses type inference to determine the type of variables where possible. So the
response
constant, for example, is fully typed, as TypeScript knows what is returned by the fetch API. If you remove the.json()
part and re-add the dot, the editor will offer you all the properties and methods available on objects of typeResponse
(this type does not need to be explicitly imported for this functionality).
Also the method parameters are already typed based their default values.For more information regarding the used weather API, please check https://open-meteo.com/en/docs. We are using this free API under non-commercial terms, by "Incorporating the service into educational content".
The response to the request looks like this:
{ "latitude": 49.32, "longitude": 8.639999, "generationtime_ms": 0.2900362014770508, "utc_offset_seconds": 0, "timezone": "GMT", "timezone_abbreviation": "GMT", "elevation": 105.0, "current_weather": { "temperature": 23.6, "windspeed": 8.9, "winddirection": 297.0, "weathercode": 3, "is_day": 1, "time": "2023-06-27T11:00" } }
-
Finally call this method at the end of the
onInit
method by inserting the following after the model has been set:this.loadWeatherData();
Again, you will not need to type the entire method name, but you can select it from the suggestion list after typing
this.l
or simply confirm the only remaining suggestion after typingthis.loadW
. -
For
this.getModel().setData()
an error is displayed, saying "Property 'setData' does not exist on type 'Model'". This is because thegetModel()
method returns the base classsap.ui.model.Model
- after all it could also be e.g. an ODataModel. What it actually is, is determined at runtime by what the application set earlier on. You know for sure it is aJSONModel
, so you can resolve this error using a type cast - basically telling the compiler which concrete model class it is:(this.getModel() as JSONModel).setData(jsonData);
There are other ways to deal with it in this case, e.g. assigning the model to a typed member variable of the controller instead of fetching it with the
getModel()
method, but sometimes type casts are needed and we are here to learn about TypeScript, aren't we?
-
When you run
npm run lint
, you will see errors and a warning reported:1:8 warning 'MessageBox' is defined but never used @typescript-eslint/no-unused-vars 12:3 error Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator @typescript-eslint/no-floating-promises 16:9 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment 17:3 error Unsafe call of an `any` typed value @typescript-eslint/no-unsafe-call
To fix the first two, remove the very first line in the file importing the unused MessageBox and write the operator
void
in front ofthis.loadWeatherData()
. This (rarely used) JavaScript operator indicates that it is ok that this asynchronous method returns later, long after theonInit()
method has been processed and left:void this.loadWeatherData();
-
The two remaining linting errors hint that you are working with variables of type
any
, which undermines the type checks. What you can do - besides disabling this rule here or in the entire app - is defining the response structure of the weather service as your own custom type! It's straightforward and can be done right in the implementation file, although in real projects you often bundle such types in one place.Insert the following definition of the weather data structure e.g. right after the
import
statements close to the beginning of the file:type WeatherInfo = { current_weather: { temperature: number, windspeed: number, winddirection: number } }
It is sufficient to define the properties we are interested in. Actually it doesn't matter here at all what we define, as the value is only set to the JSONModel, which in turn also accepts any kind of structure.
Then use this type to declare with a type cast what kind of JSON structure is provided by the fetch response:
const jsonData = await response.json() as WeatherInfo;
Trying
npm run lint
again shows: the errors and warning are gone.In the end, the Main controller should look like this:
import BaseController from "./BaseController"; import JSONModel from "sap/ui/model/json/JSONModel"; type WeatherInfo = { current_weather: { temperature: number, windspeed: number, winddirection: number } } /** * @namespace com.myorg.myapp.controller */ export default class Main extends BaseController { onInit(): void { const model = new JSONModel(); this.setModel(model); void this.loadWeatherData(); } async loadWeatherData(lat = "49.31", lon = "8.64") { // default coordinates: Walldorf const response = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true`); const jsonData = await response.json() as WeatherInfo; (this.getModel() as JSONModel).setData(jsonData); } }
After completing the following steps, the main view will display the current weather for Walldorf.
-
Open the file
webapp/view/Main.view.xml
and replace the<MessagePage>...</MessagePage>
block with the following content:<Page id="page" title="Current Weather"> <content> <form:SimpleForm> <Label text="Temperature" /> <Text text="{/current_weather/temperature} °C" /> <Label text="Wind Speed" /> <Text text="{/current_weather/windspeed} km/h" /> <Label text="Wind Direction" /> <Text text="{/current_weather/winddirection} °" /> </form:SimpleForm> <FormattedText class="sapUiResponsiveMargin" htmlText="<a href='https://open-meteo.com/'>Weather data by Open-Meteo.com</a> under <a href='https://creativecommons.org/licenses/by/4.0/'>Attribution 4.0 International (CC BY 4.0)</a> license" /> </content> </Page>
The view now contains a Page control with a
sap.ui.layout.form.SimpleForm
inside. In this form, several texts are bound to properties in the weather data loaded above.Below the form, a short text gives due credit to the used API.
-
To the list of attributes for the opening
<mvc:View...
tag, add the following one:xmlns:form="sap.ui.layout.form"
As result, the
Main.view.xml
file should look like this:<mvc:View controllerName="com.myorg.myapp.controller.Main" displayBlock="true" xmlns="sap.m" xmlns:mvc="sap.ui.core.mvc" xmlns:core="sap.ui.core" xmlns:form="sap.ui.layout.form" core:require="{ formatter: 'com/myorg/myapp/model/formatter' }"> <Page id="page" title="Current Weather"> <content> <form:SimpleForm> <Label text="Temperature" /> <Text text="{/current_weather/temperature} °C" /> <Label text="Wind Speed" /> <Text text="{/current_weather/windspeed} km/h" /> <Label text="Wind Direction" /> <Text text="{/current_weather/winddirection} °" /> </form:SimpleForm> <FormattedText class="sapUiResponsiveMargin" htmlText="<a href='https://open-meteo.com/'>Weather data by Open-Meteo.com</a> under <a href='https://creativecommons.org/licenses/by/4.0/'>Attribution 4.0 International (CC BY 4.0)</a> license" /> </content> </Page> </mvc:View>
-
The additionally used
sap.ui.layout
control library must be registered as dependency for the UI5 build tools. In the fileui5.yaml
find theframework
>libraries
section and add the following entry to the list:- name: sap.ui.layout
⚠️ Remark: the same can be achieved using the UI5 tooling by executingnpx ui5 add sap.ui.layout
Do the same in the file
ui5-dist.yaml
and make sure for both that the indentation is correct. -
This new dependency also needs be added to the application manifest. In the file
webapp/manifest.json
, find thesap.ui5
>dependencies
>libs
section and append the following entry to the list."sap.ui.layout": {}
Don't forget adding a comma after the previous entry.
-
Even if you have
npm start
still active, which means the UI5 tools run in watch mode, these changes to the UI5 project configuration (adding the sap.ui.layout library) require a restart. Hence please stop thenpm start
process in case it is still running and launch it again.
The app now displays current weather data for Walldorf.
You've now loaded data and extended the UI to display it.
Along the way, you have learned a LOT about TypeScript in UI5 apps:
- how it allows using modern JavaScript syntax with ES6 classes and modules
- that it provides inline documentation and a way to navigate to the definitions
- where to typically spot the little actual TypeScript syntax among all the JavaScript
- how it helps with automatic code completion, adding imports and "Quick Fixes", which both reduce the amount of required typing
- how it types variables via type inference even when you do not provide type information
- that you have to provide the namespace via JSDoc for classes you define
- how linting makes you aware of possible issues beyond the regular type chacking
- how you can use and define your own custom types.
Continue to - Exercise 3 - React on User Input