Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
WouterLemaire
Active Contributor
Around April SAP announced for the first time TypeScript Support for UI5. Since then, I’ve tried to use this in every new UI5 project where possible. Now it is time to share my experience with TypeScript in UI5 in this blog post series:

After implementing a TypeScript version of the Service wrapper, that allows you to use the UI5 ODataModel v2 with promises, it is time to take the project to the next level.

In this blog post, I’m going to bring in classes and a state/model manager.

You can view the videos or continue reading the blog post. This blog post is explained in more detail

in two videos.
First one starts with the implementation of classes:



The second video will be about the state/model manager:


Classes


In the previous blog post, I used the Service wrapper to fetch the data from the OData service and store it in a variable of the related type. Now, I will do the same thing but pass this data to an object instance of a related class. We will use the type to map the properties of the result with the properties of the class instance.

Before we can create classes for the data of the NorthwindService, I created a BaseObject class that can be used for reusable logic that other classes can consume by extending from it.

Add a BaseObject.ts file in the model folder:


In this class I added some reusable functions like:

  • A busy property which can be used to bind to a UI Element

  • zeroPad to add leading zero’s


At the moment, it doesn’t contain many functions but having this class allows us to use it as soon we have more functions that can be reused across classes. It contains an important abstract function, the “getJSON” function, this will help me to get the data in the format the OData expects it. Every class will have to implement this for its related OData entity.

The BaseObject class comes with a generic type which needs to defined when another class extends from it. This generic type allows me to provide a type for the getJSON function when extending from it.
import Object from "sap/ui/base/Object";
/**
* @namespace be.wl.TypeScriptDemo.model
*/
export default abstract class BaseObject<T> extends Object {
private busy = false;
constructor() {
super();
this.busy = false;
}
public setBusy(busy: boolean): void {
this.busy = busy;
}
public zeroPad(num: number, places: number): string {
const zero = places - num.toString().length + 1;
return `${Array(+(zero > 0 && zero)).join("0")}${num}`;
}
public abstract getJSON():T;
}

BaseObject.ts: https://github.com/lemaiwo/UI5TypeScriptDemoApp/blob/main/src/model/BaseObject.ts

 

After that I created classes for the data which I will fetch from the Northwind service, Address and Supplier.


Both classes extend from BaseObject and will have properties for every property of the related OData entity in the Northwind service. In the constructor the properties from the service will be mapped to the instance of the class.

The getJSON will do the same in the other direction, for sending data back.

Address class:
import { AddressEntity } from "../service/NorthwindService";
import BaseObject from "./BaseObject";
/**
* @namespace be.wl.TypeScriptDemo.model
*/
export default class Address extends BaseObject<AddressEntity>{
private street:string;
private city:string;
private state:string;
private zipCode:string;
private country:string;
constructor(data?:AddressEntity){
super();
if(data){
this.street = data.Street;
this.city = data.City;
this.state = data.State;
this.zipCode = data.ZipCode;
this.country = data.Country;
}
}
public getJSON(): AddressEntity {
return{
City:this.city,
State:this.state,
Country:this.country,
Street:this.street,
ZipCode:this.zipCode
};
}
}

Address.ts: https://github.com/lemaiwo/UI5TypeScriptDemoApp/blob/main/src/model/Address.ts

Supplier class, which will have a property for the instance of class address:
import { SuppliersEntity } from "../service/NorthwindService";
import Address from "./Address";
import BaseObject from "./BaseObject";

/**
* @namespace be.wl.TypeScriptDemo.model
*/
export default class Supplier extends BaseObject<SuppliersEntity>{
private id:number;
private name:string;
private address:Address;
private concurrency:number;
constructor(data?:SuppliersEntity){
super();

if(data){
this.id = data.ID;
this.name = data.Name;
this.concurrency = data.Concurrency;
this.address = new Address(data.Address);
}
}
public getJSON(): SuppliersEntity {
return {
ID:this.id,
Name:this.name,
Concurrency:this.concurrency,
Address:this.address.getJSON()
};
}
}

Supplier.ts: https://github.com/lemaiwo/UI5TypeScriptDemoApp/blob/main/src/model/Supplier.ts

State/model manager


In the next step, I’m going to add a state/model manager. I use this to get the data from the OData service (using the Service object) and put it into an instances of the classes. Because the mapping already happens in the constructor of the classes, I only need to pass the result to the constructor of the class.

Before creating this state/model manager, I add a basestate class. This contains some reusable functions related to the state/model manager which might be valuable if you have more than one state/model manager.

Add the basestate in the state folder:


The BaseState contains following functions:

  • constructor which has a property service defined with a generic type. The state/model manager should pass the service wrapper instance (NorthwindService in this case).

  • getService is the function that will return the service wrapper instance passed in the constructor

  • getModel will create a new JSONModel the first time it is being called, from then on it will return the earlier created JSONModel

  • updateModel is a wrapper function to refresh the JSONModel if it exists

  • getData will return the data object. The data object will contain everything that can be bound to the view which will be managed by the state/model manager. This is also defined by a generic type which can be set by every state/model manager that extends from this class.


import Object from "sap/ui/base/Object";
import JSONModel from "sap/ui/model/json/JSONModel";
import BaseService from "../service/BaseService";
/**
* @namespace be.wl.TypeScriptDemo.state
*/
export default abstract class BaseState<T extends BaseService,R extends object> extends Object {
protected service: T;
protected data: R;
private model: JSONModel;
constructor(service:T) {
super();
this.service = service;
}
public getModel(): JSONModel {
if (!this.model) {
this.model = new JSONModel(this.data)
}
return this.model;
}
public updateModel(hardRefresh?: boolean): void {
if (this.model) {
this.model.refresh(hardRefresh ? true : false);
}
}
protected getService(): T {
return this.service;
}
protected getData(): R {
return this.data;
}
}

BaseState.ts: https://github.com/lemaiwo/UI5TypeScriptDemoApp/blob/main/src/state/BaseState.ts

After creating the BaseState class I created a new class NorthwindState which extends from BaseState in the state folder:


At the top of this class, I created a type for the data object to set the generic type in the BaseState together with the type of the service wrapper instance (NorthwindService). With that, everything has a type, even the functions in the BaseState class.

In the constructor I pass the service to the BaseState and set the initial version of the data object.

Next to that, I added a function to get a specific supplier “getSupplier”. I use the NorthwindService to get a supplier with the type of the entity and pass it to a new Supplier instance. This instance will be stored in the data object which will again update the view as soon as “updateModel” is executed.
import Supplier from "../model/Supplier";
import NorthwindService from "../service/NorthwindService";
import BaseState from "./BaseState";

export type nwdata = {
supplier:Supplier
};

/**
* @namespace be.wl.TypeScriptDemo.state
*/
export default class NorthwindState extends BaseState<NorthwindService,nwdata>{
constructor(service:NorthwindService){
super(service);
this.data = {
supplier: new Supplier()
}
}
/**
* getSupplier
*/
public async getSupplier(id:number) {
const supplierEntity = await this.getService().getSupplierById(id);
this.getData().supplier = new Supplier(supplierEntity.data);
this.updateModel();
return this.getData().supplier;
}
}

NorthwindState.ts: https://github.com/lemaiwo/UI5TypeScriptDemoApp/blob/main/src/state/NorthwindState.ts

Connect the dots


Connecting the dots to get the data object available in the view for bindings. I create an instance of the NorthwindService and NorthwindState in the Component. The main OData v2 model will be passed to the service wrapper object. The NorthwindService will on its turn be passed to the NorthwindState.


Component.ts: https://github.com/lemaiwo/UI5TypeScriptDemoApp/blob/main/src/Component.ts

 

In any controller of a view that you want to bind to the data object of a state/model manager you have to:

  • Get the state from the component

  • Set it as model to the view


App.controller.ts


App.controller.ts: https://github.com/lemaiwo/UI5TypeScriptDemoApp/blob/main/src/controller/App.controller.ts

In the view I can now simply bind to any property/object in the data object of the state/model manager:


App.view.xml: https://github.com/lemaiwo/UI5TypeScriptDemoApp/blob/main/src/view/App.view.xml

 

Result


The result is a simple form filled by using the NorthwindService for fetching the data which is converted into instances of classes by the NorthwindState with classes that take care of the mapping:


 

 

Labels in this area