CRM and CX Blog Posts by Members
Find insights on SAP customer relationship management and customer experience products in blog posts from community members. Post your own perspective today!
cancel
Showing results for 
Search instead for 
Did you mean: 
mkoball
Explorer
1,635

Typescript for CCO plugin

  • Motivation
  • What is typescript and what are the benefits of using it
  • setup
  • install node and typescript
  • type definition
  • running tsc and clean up of output javascript
  • codehaus maven plugin
  • How to gather types
  • JS-Docs as alternative
  • Conclusion

Motivation

This blog post is directed to developers that already have some experience in developing Plugin for CCO and might be frustrated by the experience of creating customer UI components.
If you do not have any experience in CCO development of plugins, I would suggest to check out other resources like sap-customer-checkout-plugin-development  first.

When creating a plugin with custom UI element like generic-pop-ups or generic-inputs wou likely encountered errors like this:

 

<VM1276:1 Uncaught TypeError: Cannot read properties of undefined (reading 'getId')
at <anonymous>:1:19>  

 

or

 

<VM1682:1 Uncaught TypeError: Cannot set properties of null (setting 'test')
at <anonymous>:1:7>

 

And you also did catch yourself rebuilding a plugin just to inspect the field of the receipt in the browser dev tools, so that you can try to avoid these errors.
You might also still copy your first generic popup config into new project to know the structure of them.

In this blog post we will examine our options to use type definitions to counter these pain points.

What is typescript and what are the benefits of using it

Typescript is a programming language developed and maintained by microsoft.
It is a superset of javascript with the goal to increase development productivity and safety by adding static type checking to the language.
Typescript itself does not have a runtime. It needs to be transpiled down to javascript.

The benefit of static type checking is that you will get hints and autocompletion in your IDE based on the types of the object you are working with.
This can help you catch potential bugs earlier and avoid unnecessary rebuilds.

ts_img_1.png

ts_img_2.png

Setup

install node and typescript

Since browsers only can work with javascript we will need to transpile our typescript source files into javascript.
For this task we will need to install node and typescript.
To install node by following the instruction for your system on https://nodejs.org/en/download/package-manager.
I would recommend using nvm to install and choosing the latest LTS version which is version 20 while writing this blog.

After verifying that you have node installed run

 

npm install -g typescript

 

To install typescript globally on your system

type definition

This tutorial will not cover all the features off the language.
For general information about typescript, please refer to the official documentation https://www.typescriptlang.org/docs/

But a very basic overview:
You can define a type of object in typescript like this:

 

import {Permission} from "./types";
type User = {
  name: string;
  id: string;
  key: string;
}
//you can use those types like so:
const myUser: User = {
  id: "A_ID",
  name: "A_NAME",
  key: "a9c43450-85da-409e-9524-bb75d8cfd433"
}


//trying to use it like this will error like so TS2339: Property fullName does not exist on type User
myUser.fullName = "FULL_NAME"

// you can use custom types for new type definitions as well
type Permission = {
  permissionParameters: any[];
  allowed: boolean;
  protectedResourceId: string;
};
type User = {
  name: string;
  id: string;
  key: string;
  permissions: Permission[];
}


// if you want to use the type in a different file you should add export
export type User = {
  name: string;
  id: string;
  key: string;
  permissions: Permission[];
}

//if you want to use those types in a different file you need to import them
import {User} from "./myTypes"


//you can also define interface that can extended or be implemented by classes
interface EventBusSubscriber {
  handleEvent(event: Event): boolean;
}

// extended has method handleEvent as well
interface UiPlugin extends EventBusSubscriber {
  pluginService: PluginService;
  eventBus: EventBus;
}

// this class now needs have the two fields and to implement the method
class MyPlugin implements UiPlugin {
  eventBus: EventBus;
  pluginService: PluginService;

  handleEvent(string): boolean {
    return true;
  }

}

 

As can imagine it might get complex to create all of those types by hand. Refer to the "How to gather types" section for some help with that.
Recommendations for CCO plugins:
I would recommend to maintain type definitions of CCO types in a separate typescript file (for suggestions to gather those types see below) .
Project specific types can be maintained at the bottom of your main frontend code.

transpiling typescript

NOTE: if you do not want to add something to your build process and or do not want to add dependencies to you project you still might consider checking out the last section which will use JSDoc annotation instead of writing actual typescript for your plugin code. As mentioned we need to transpile typescript to javascript to use the code.
For this step we will use the tool "tsc" which becomes usable by installing typescript

generally you can transpile all typescript files in a directory to javascript files by running

 

tsc -p <path/to/your/ts-sourcefiles>

 

Since our output javascript need be structured more specific for CCO plugins we will need to set some compiler options.
We can do this in a file called tsconfig.json. You can create yourself one of those by running:

 

tsc -init

 

After testing a little bit this is a good stating point for cco plugins:

 

{
  "compilerOptions": {
    "target": "es2022",
    "module": "es2022",
    "allowJs": true,
    "checkJs": true,
    "noImplicitAny": true,
    "removeComments": true,
    "preserveConstEnums": true,
    "allowSyntheticDefaultImports": true
  },
  "exclude": ["cco-types.ts", "*.js"],
  "linterOptions": {
    "exclude": ["cco-types.ts"]
  }
}

 

The only problem with this setting is the following:
Since we have set the setting module like this "module": "es2022". We will get an export {} at the bottom of the file when we import types from a second file.
The easiest way to get around I found is to run a little js script to strip that export. Here is the source for this:

 

import * as fs from "fs";
/**
*  {fs.PathOrFileDescriptor} filePath
*/
function removeExportEmptyObject(filePath) {
  try {
    let content = fs.readFileSync(filePath, 'utf8');
    let newContent = content.replace(/export\s*{\s*}\s*;?\s*$/gm, '');
    if (content !== newContent) {
    fs.writeFileSync(filePath, newContent, 'utf8');
    console.log(`export {} removed from ${filePath}`);
    } else {
      console.log(`no export {} found to be removed from ${filePath}`)
    }
  } catch (err) {
  console.error(`Error removing export {}: ${err.message}`);
 }
}
fs.readdir("./src/main/resources", function (err, files) {
  if (err) {
    return console.log('Unable to scan directory: ' + err);
  }
  files.forEach(function (file) {
    if (file.endsWith("js") && file !== "remover.js"){
      removeExportEmptyObject(`./src/main/resources/${file}`);
    }
  });
});

 

Since it would be tedious to remember to run those in succession I find it easier to use npm (which comes installed with node) for that

 

npm init

 

in the root of your plugin directory to create a package.json file. Add a new line in the script section.
A complete package.json might look like this:

 

{
  "name": "pluginui",
  "version": "1.0.0",
  "description": "",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build-ts": "tsc -p ./src/main/resources && node 
        src/main/resources/remover.js"
    },
  "author": "",
  "license": "ISC"
}

 

After this you can build your .ts source files in the src/main/resources directory into .js files by running:

 

npm run build-ts

 

You could use it like that, but I would recommend one final configuration.

By adding the codehaus mojo plugin you incorporate the typescript transpile step into the build process of your plugin

I will use maven as an example:
You will have to add a plugin to the build.plugins section

 

<build>
  <!-- other_plugins-->
  <plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>3.0.0</version> <!-- Use the latest version -->
    <executions>
      <execution>
        <id>run-tsc</id>
        <phase>validate</phase>
        <goals>
          <goal>exec</goal>
        </goals>
        <configuration>
          <executable>npm</executable>
          <workingDirectory>${project.basedir}</workingDirectory>
          <arguments>
            <argument>run</argument>
            <argument>build-ts</argument>
          </arguments>
        </configuration>
      </execution>
    </executions>
  </plugin>
</build>

 

And that it is! You now are able to create to write your frontend code in typescript and build your project like normal (so for me that is just the standard mvn clean package)

side note: if you have an internal process to build and or deploy you plugins you might need to have some changes there. Mostly wherever your pipeline might run you will need to have node and typescript installed as well.

How to gather types

Some of the objects in the cco plugin are quite complex, and it would be tedious to do so completely by hand.
Especially for all Entity and dto types you do not want to do that by hand.
So one way to speed up the process would be to make use of the debug mode of the cco frontend and using some js directly in the browser.

To enter the debug mode of cco simply add "?DEBUG=1337" to the end of the URL in you browser with CCO opened.
(e.g. if you cco instance is accessible from http://localhost:9999/1337_/ simply change it to "http://localhost:9999/1337_/?DEBUG=1337")
Doing so will expose the "cco" namespace, the "ccoPluginService" and the "ccoEventbus" to the console

Then I have created this bit of JS code:

 

function getTypeScriptDefinition(obj, typeName = 'MyType') {
  const typeDefinitions = [];
  const processedObjects = new Map();

  const collectPropertyDetails = (targetObj) => {
    let propertyDetails = {};
    let propertyNames = Object.getOwnPropertyNames(targetObj);
    let propertyDescriptors = Object.getOwnPropertyDescriptors(targetObj);

    propertyNames.forEach((propName) => {
      const propDescriptor = propertyDescriptors[propName];
      const propValue = propDescriptor.value;
      const propType = Array.isArray(propValue) ? 'array' : typeof propValue;

      propertyDetails[propName] = propType;
    });

    return propertyDetails;
  };

  const getDetailedTypeInfo = (obj) => {
    let detailedTypeInformation = {};

    Object.assign(detailedTypeInformation, collectPropertyDetails(obj));

    let prototypeChain = Object.getPrototypeOf(obj);
    while (prototypeChain && prototypeChain !== Object.prototype) {
      Object.assign(detailedTypeInformation, collectPropertyDetails(prototypeChain));
      prototypeChain = Object.getPrototypeOf(prototypeChain);
    }

    return detailedTypeInformation;
  };

  const generateTypeScriptDefinition = (obj, typeName) => {
    if (processedObjects.has(obj)) {
      return processedObjects.get(obj);
    }

    const detailedTypeInfo = getDetailedTypeInfo(obj);
    let tsDef = `export type ${typeName} = {\n`;

    for (let [key, type] of Object.entries(detailedTypeInfo)) {
      let tsType;
      if (type === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
        const nestedTypeName = key.charAt(0).toUpperCase() + key.slice(1);
        tsType = nestedTypeName;
        typeDefinitions.push(generateTypeScriptDefinition(obj[key], nestedTypeName));
      } else if (type === 'array') {
        const arrayElemType = obj[key].length > 0 ? typeof obj[key][0] : 'any';
        if (arrayElemType === 'object') {
          const nestedArrayTypeName = key.charAt(0).toUpperCase() + key.slice(1, -1);
          tsType = `${nestedArrayTypeName}[]`;
          typeDefinitions.push(generateTypeScriptDefinition(obj[key][0], nestedArrayTypeName));
        } else {
          tsType = `${arrayElemType}[]`;
        }
      } else if (type === 'function') {
        tsType = 'Function'
      } else {
        switch (type) {
          case 'string':
            tsType = 'string';
            break;
          case 'number':
            tsType = 'number';
            break;
          case 'boolean':
            tsType = 'boolean';
            break;
          case 'object':
            tsType = 'any';
            break;
          default:
            tsType = 'any';
        }
      }

      tsDef += `  ${key}: ${tsType};\n`;
    }

    tsDef += '};';
    processedObjects.set(obj, tsDef);
    return tsDef;
  };

  const mainTypeDef = generateTypeScriptDefinition(obj, typeName);
  typeDefinitions.unshift(mainTypeDef); // Ensure the main type definition is at the top

  return typeDefinitions.join('\n\n');
}

 

I won't go into too much detail what it does but just the basic idea:
It uses the build in methods Object.getOwnPropertyNames and Object.getOwnPropertyDescriptors to get the property names and their descriptors.
It then uses Array.isArray, Object.getPrototypeOf and the typeof function to get type information.
If an object is encountered it will call itself recursively. At the end the function should return a string of type definitions that wou can copy and paste into a typescript file.

How to use this function?
1. simply copy and paste it into your browsers console (since it is only a definition you will get back an undefined printout)
2. then call the function like so:

 

console.log(getTypeScriptDefinition(<any_object_which_types_you_want_to_know>,"<the_name_of_type>"))

 

e.g. for the currently logged in user

 

console.log(getTypeScriptDefinition(ccoPluginService.getContextInstance('UserStore').getUser(),"User"))

 

for this input it will return

 

export type User = {
  name: string;
  id: string;
  key: string;
  info: Info;
  permissions: Permission[];
  drawerId: any;
  constructor: Function;
  setName: Function;
  getName: Function;
  setId: Function;
  getId: Function;
  setKey: Function;
  getKey: Function;
  setInfo: Function;
  getInfo: Function;
  getLanguageCode: Function;
  getPermissions: Function;
  getPermission: Function;
  getDrawerId: Function;
};

export type ModifiedAt = {
  date: number;
  hours: number;
  seconds: number;
  month: number;
  timezoneOffset: number;
  year: number;
  minutes: number;
  time: number;
  day: number;
};

export type CreatedAt = {
  date: number;
  hours: number;
  seconds: number;
  month: number;
  timezoneOffset: number;
  year: number;
  minutes: number;
  time: number;
  day: number;
};

export type Info = {
  alternateIds: any[];
  additionalFields: any[];
  modifiedAt: ModifiedAt;
  autologoutMaxIdleSeconds: number;
  passwordChangeForced: boolean;
  employeeID: string;
  locale: string;
  formattedNameSalesPersonStyle: string;
  createdAt: CreatedAt;
  terminalUserId: string;
  modifiedBy: string;
  key: string;
  externalAuthentication: boolean;
  recoveryUser: boolean;
  posUnitUiMode: any;
  priceListId: string;
  languageCode: string;
  userName: string;
  actionAfterTransaction: string;
  consentMaintained: boolean;
  navigationBarColor: string;
  formattedNameCashierStyle: string;
  createdBy: string;
  name: string;
  consentVersionCode: string;
  salesPerson: boolean;
  autolockMaxIdleSeconds: number;
  salesPersonID: string;
};

export type Permission = {
  permissionParameters: any[];
  allowed: boolean;
  protectedResourceId: string;
};

 

et violà: you have some type definitions
NOTES:
even tough the javascript code does have some caching implemented, it will likely create stack overflows if you call it on to big objects.
As you can see functions like getId are only mapped to the type Function. You might consider manually adding argument types and return types by hand if you find your self using them often.
(as shown in the type definition section)
JSDoc as alternative to typescript source

As mentioned before you might not want to introduce an extra step to your build process.
If that is the case, but you still want to get most benefits of type safety you can use JSDocs annotations with type hints.
To get those you do not have to do much. Simply remove the "*.js" form the exclude parameter in you tsconfig.json (refer from the "transpiling typescript" section)

consider this example

 

// file: types.ts
export type Foo = {
    id: number,
    counter: number 
}
export type Bar ={
    foo : Foo,
    increment: ()=> void
}
/**  {import("./types").Foo} foo */
function myFunction (foo){
    foo.other = "test"
    /** @type {import("./types").Bar} */
    const bar = {
      foo: foo, 
      increment:()=>this.foo =this.foo + 1 }
    bar.increment()
}

 

 

 

While typing these you will notice that you get the same autocomplete as if wou where writing typescript.

ts_img_3.png

ts_img_4.png

Since all the type information are provided in comments this file is just normal javascript and will be understood by the browser without any additional steps

Even if this seems very verbose at first, modern IDE (like intellJ in my case) can help you a lot with this. You could set up live templates to write this /** @type {import("./types").} */ for you and you will get autocomplete from there.

mkoball_0-1722428870164.png

Conclusion

In this blog post, we explored options for improving type safety when writing frontend code in CCO Plugins.
Even though it might take some time to gather all the types and to get it all set up.
But in my opinion this time is well spent as it greatly increases development speed and safety when it actually matters.
I highly recommend giving it a try if you are interested

1 Comment