Skip to Content
Personal Insights

How I built on-prem xsa cap based solution incluing hana flp and cordova apps with ui integration cards

Introduction

In case you are interested in the recent sap capm topic and the new fancy tools, I’d suggest you take a look at this nice and fresh blog post by Mio Yasutake  where you can see how to build and deploy your cap project into scp portal flp.

I guess, this is what “future” looks like in terms of development and deployment.

In this post we are going to take a look at the “past” for as you could guess from the title of the post, we are going to use on-premise (virtualized actually) hana hxe to build and deploy self-contained solution with a bonus “legacy” content – mobile apps built with ui5 and cordova.

Of course, we will use webide shipped with our hxe 2 sp04 with basic template support for our needs.

If this looks oldschool enough for you – lets dig deeper )

 

Key takeaways

Before wasting too much of your time I would highlight some topics I wanted to share my insights about:

  • cap – how to get both cap-provided odata and “freestyle rest” endpoints in your solution and what can go wrong
  • flp – how to add flp site with fiori elements apps/modules using the cap odata proxy
  • cordova – how to bundle your apps and have it successfully validated in the app store
  • ui5 integration cards – how to add loosly coupled widgets to your ui5 app with offline support
  • random ui5 stuff – as I made the full source code available, you can probably find some useful snippets in the code

 

Setup

Some years ago when notebooks had “just” 4gigs of ram I got myself an Intel NUC rig to play with hana express edition.

There I installed esxi hypervisor, so I got some virtualized lab capable of running hana and some other useful vms.

So, for the solution described here we have:

  • Centos 7 box with hxe installed via binary dist method
  • Centos 7 box with nginx which we will use as a reverse proxy
  • Auxiliary gitlab box for obvious purpose

Of course we also have some domain names (I have one for the “internal” system and another one – “external” – for the solution itself) and real ip address to make our host visible from the internert.

Again, no docker or any modern stuff allowed here as we are oldschool, remember? )

 

Rationale

Suppose, we want to make a fitness mobile solution to sell personal gym workouts.

It would obviously consist of a Client mobile app where users would browse content, search local gyms for coaches, purchase subscriptions and eventually arrange the workouts to be performed either in gym together with coach, or online (with “remote” coach)

Also we would need the counterpart Coach mobile app where coaches could react to the scheduled events (workouts) by either creating the workout plan based on the exercises content they have (for online workouts) or by attending the local gym and then filling in the workout report.

From the administrative perspective we want our clients to have unrestricted access to our solution, while coaches would be registered in the system first, and then somehow onboarded into the app.

Therefore we would like to have some admin app suite to manage master data, settings and browse some reports.

And this is what we are actually going to build using aforementioned tools.

Blog structure and possible updates

Upon writing this stuff I understood it may be too big to read in one take.

As it took me the whole day to write it and another one to edit, I feel like I don’t have much desire to rework it until some questions or comments would require me to update it.

Then I might consider splitting it into parts and adding some more specific information.

 

Solution

Project structure

From the solution perspective it is a single mta based on cap project structure:

  • db module contains regular cds artifacts
  • srv module (gen folder added via cds build) contains service definitions and generic admin service with handlers
  • srv_mobile (gen folder must be copied here) contains custom express app with endpoints for mobile apps (/coach and /client) with both cap (for /odata) and “native” express (for /rest ) handlers
  • ui_fe* modules contain fiori elements apps (odata v2 via proxy) for admin flp
  • flp and flp-Content are xsa flp site which consume fe apps via destinations
  • ui5_clientApp and ui5_coachApp are runnable modules from which cordova apps are made

Source code

For this blog I made a snapshot of my gitlab repo on the github so that you could get into the complete source in case you’re interested: https://github.com/myakinkii/cap-xsa-flp-cordova

Highlight: To make links from the blog to this repo look different from other “outside” links, I will mark it with curly braces like this: {link to readme}

Disclaimer: as the state of the project is wip, not every part of the solution has the productive usage readiness. In fact, the mobile backend provides only rudimentary authentication and authorization checks. The code is provided for the demo and educational purposes. Author has no responsibilty over the possible issues while using the code in productive scenarios.

 

Development and deployment

Dev

First, from the technical perspective we would also like to have our development landscape somehow isolated from the productive one, so we’ll brfiefly talk about spaces

For this solution I have the default hxe “development” space with di builder that deploys everything right from the webide, so I can test most parts of the solution without doing the cordova builds.

As our srv_mobile module is a single express app, we just need the single location definition with corresponding upstream in our reverse proxy – this is how I test mobile apps in dev.

From the admin apps perspective, as they are a separate modules, they are runnable as separate apps, so I can test it right in webide without external access.

Test

Once we have tested our stuff developed, we build the mtar and deploy it to another “test” space

For this space we also have an location and upstream for mobile apps.

As for the admin apps (as they are already bundled into the flp), we have a little bit different approach.

My hana instance has the fqdn different from the “public” domain I have for the solution itself.

And as the flp requires authentication, it redirects you to the uaa-security page which obviously has internal domain name redirect and own port.

So, I had to expose those ports without proxying it.

And as my hxe domain was a proper fqdn it worked just fine.

SSL

Another important moment here – as our hxe has a self-signed certificate, we have a huge benefit by making our proxy (which has valid letsencrypt ssl certificate) to handle our backend requests.

More about that in Cordova part though.

 

CAPM backend

Modules, app init and bootstrapping

Ok, lets start with backend design

The default project layout and build tasks has our cds artifacts to be in {db} folder, from where it is deployed to hdi container, while odata service definitions and handlers and resulting scn file (whole service representation stuff) is generated and served from {srv} folder.

The admin backend uses {server.js} bootstrap file to setup odata v2 proxy according to the docs by Gregor Wolf

From hana xsa perspective that would mean our db module is a simple deployer (as for example flp-content), while srv is a proper express app that starts listening on some http port.

So, to enable authentication in that app we would need to add xsuaa service and have our clients authenticate via the login page (through the redirect).

From our solution perspective this makes sense from the admin suite perspective, but does not fit the mobile part, because we want the service to be publicly exposed and then handle the onboarding and authentication itself with no redirects and sso stuff.

So, this is the reason I wanted to have my own express app serving both the cap generated service with cap handlers and my own rest endpoints with express handlers.

And this is what we have in {srv_mobile} folder. Again, as I was not able to easily find information how to make cds generate runtime artifacts to non-standard folders/modules, I simply copy the gen folder from srv to srv_mobile.

To set this stuff up I actually got the example from the aforementioned odata proxy docs with CDS Combined Backend Custom example.

The main difference between the srv and srv_mobile is that in the second case we have our own “standalone” {index.js} where we instantiate our “regular” expressjs app – and this is exactly what we wanted.

So, with some closures involved, we have access to both cds cap srv object and the express app in our service handler.

cds.serve("ClientService").in(app).with(function(srv){ ... });

Onboarding and Authentication

After the express app is started, we have to pairs of endpoints serving our requests:

/rest/client + /odata/client for {ClientService} and /rest/coach + /odata/coach for {CoachService}

As we tend to used odata v2, we define “odata” prefix in {proxy setup} for the whole app whichs is followed by the service path from its definition

To make the “rest” prefix look similar, we define {service specific baseUrl constant} to be used in express handlers.

The request handling flow is pretty simple: {unless} client provides some mandatory parameters (device and token) with each request (currently as url parameters, not as headers), we reject the requests.

Security flaw: Currently unless we implement a custom handler, we do not even check the validity of this pair.

To obtain the token client must request the /onboard rest endpoint, which would register him and provide with token in return.

Strictly speaking, we have some sort of insecure basic authentication with randomly generated logins and passwords.

As the Client app is available for anyone to use, we have no checks in our {/rest/client/onboard} endpoint, so we either create or return the Profile.

In the CoachSercvice case we have restricted access to the app, so the suite administrator must first create the coach profile and share the qr code with correct token with user.

Upon receiving the request, our {/rest/coach/onboard} endpoint will try to find the record and update it with new creds.

Major consideration point for custom handlers

So, mostly the handlers I implement are pretty straightforward from the business logic perspective.

But there’s one thing it took me a while to grasp: I do not implement backend server, I implement backend client.

Let me clarify this thought.

As you might read in docs or see in open sap videos you implement your custom handlers via the connection to some entity, called service.

What would that mean, is you only have access to stuff that is exposed in those services. And the service defines the access level (readonly or full crud)

And as it seems obvious at first, once you start implementing some real world cases, it becomes less clear how to deal with that.

So, consider the scenario:

  1. I want to make Purchases entity exposed via mobile endpoint so that I could create purchases via function import.
  2. I also want to create Payments to understand whether the the payment was complete or abandoned under the hood.
  3. What I don’t want is Payments to be exposed or at least modifiable.
  4. But in case I mark it with @readonly annotation in cds, I cannot create one in my own service handler.

So, each time I have this type of scenario I must somehow protect those auxiliary exposed entities from “direct” modifications.

Native procedures to the rescue

As I figured out how much this stuff hurts, I started thinking what would help dealing with those cases.

And some of you might remember there was a way back in those hana xsc xsodata days to specify sqlscript procedure as an exit handler for your function import or any other supported event.

And I tried to find this in documentation. To no avail.

Actually now there is a hint in docs that this is somehow possible, but a month ago it looked completely different.This stuff also looks suspicious, as this implies our some_stored_procedure must be somehow exposed from the db level to service level (we are still a client, remember):

cds.run (`CALL Some_Stored_Procedure (11,'foo')`)

Of course we have our hana guru Thomas Jung with his videos which I highly recommend watching in case you are digging in the cap stuff.

So, it turns out, there is a way to do something like that, but rather a workaround than standard cap feature.

Actually, when I first watched that video, I missed the fact, that it involves manual instantiation of a database connection object/driver and calling the stored procedure the “native hana” way.

So again, this is definitely not a part of CAP itself.

To me it seems that even though CAP tends to be database agnostic, there still must be a way to get into the db level (like they show in the example above), but it seem yet to be documented or even implemented.

In fact, there is at least one thing that CAP allows you to do, which seem to be hana specific (at least cds warns you about it). It is a {view with parameters}, which is compiled into the function (not as a view with parameters which hana supports).

And I even was not able to read from it in my custom handler 🙂

Some other issues I came up with

“View as select from …” side effects

Another unexpected result for me (probably I missed something in the documentation) was that once I defined some {random projection} to the Purchases entity as view, I lost the navigation property to the real Purchases entity in Workouts entity.

The funny thing is that at least {the first projection} seem to work fine.

AFTER handler limitation

This was another wtf moment for me, but this is clearly stated in documentation: “Only synchronous modifications are allowed. … Reasoning: Multiple after handlers can be added by different involved packages, including extension packages, and each of which would expect the same input. If one would exchange the results, the chain would break.”

In my case I was lucky to be sure that my settings object would be retreived from the db earlier at {/init} endpoint (there you can see a call to the {getCards} function which is not inside our closure because we want to reuse it for both services, so we directly pass srv instance to it), otherwise only useful application for this would be the bookshop “hardcoded discount” scenario (as I can perform some row-based calculations inside cds).

Fluent expressions and cql/cqn syntax

Well, I might as well leave this section empty as is the documentation entry (it seems they even deleted the link to that section), but what really helped me find a way to write some expressions was this file: https://github.com/SAP-samples/cloud-cap-samples/blob/master/test/cds.ql.test.js

With trial and error I was able to write the statements I needed, and it worked fine resolving associations and applying filters for me.

Setting the filters aka analytical privileges in BEFORE handler

It took me a while to find this out in the documentation (and I can’t find it again in current docs 🙁 ), so I’ll include the example here in hope it saves some time for you:

srv.before('READ', MyMoney, req => {
	return basicChecks(srv, req._.req.query.device,req._.req.query.token).then(function(profile){
		req.query.where({ "id":{"=":profile.id } });
		return Promise.resolve(profile.id);
	}).catch(function(err){
		if (!err.errCode) req.reject(400,{errCode: errors.UNKNOWN_BACKEND_ERROR, errObj:err});
		else req.reject(400,err);
	});
});

As you can see, you might add filter expression to the req object itself.

In case you need to read db to retrieve your filter values, make sure you return thenable either via direct return Promise or by marking your function async.

Overall CAP Thoughts

It was sometimes a little bit frustrating (to say the least) trying to make this stuff work, but hopefully it will one day reach the stage where the documentation covers the most aspects.

Now it looks like the work in progress internal project which is publicly marketed ahead of the implementation.

FLP

Ok, now things get a little bit easier and more predictable thanks to the fact that flp was introduced to hana xsa more than three years ago in SP1.

For example, deployer worked fine with self-signed certificate this time )

Basically the only thing worth mentioning here is that in docs it is shown how to embed your apps into the flp module. But thats not very useful for the scenario when you already have your standalone modules like we have in our solution.

So, the important thing to notice here, is the flp module is configurable as a regular app router.

So, by using the {provides} and {requires} sections in our mta, we can make our flp module {use destinations} to other ui modules in mta to get to the flp apps.

Of course, you can even have cross mta dependencies, so Bradley Smith in his great blog post explains how to do that: https://blogs.sap.com/2020/02/03/xsa-fiori-launchpad-on-premise-configuration-with-cross-mta-dependencies./

Just in case, the concept of {site-content.json} of deployer module repeats FES FLPD logic:

  • Your Apps are technically assigned to Catalogs
  • (Tile)Groups are just containers (modifiable by users) to Tiles
  • Tiles are added into (Tile )Groups and in turn are “Shortcuts” to the navigation intent
  • Navigation intent is an abstraction layer consisting of Semantic object and Action to be resolved into the actual app at runtime via Launchpad services based on User groups and Catalogs
  • (Tile )Groups and Catalogs are added to User Roles which are assigned to users

What is different from the S/4 world is that instead of configuring the target mappings, we identify apps via the {appDescriptors}, to be found by ID either embedded in site-content.json “applications” section as in Bradley’s example, or in {applications} directory as in our case.

Also of course in case we define Scopes, Groups and other xsuaa configuration options, we deal with xsa Role Templates and Role Collections in xsa-admin app instead of SU01 🙂

Here we do not set up any scopes to configure the xsuaa service, so we just want our user to be authenticated against the xsuaa to access the site and get “root” access.

Cordova

This is probably the saddest part of my blog.

Personally, I loved the Hybrid apps approach with Cordova+Kapsel and SMP or SCPMS a lot.

From my point of view, the vast widget toolkit of ui5 and the effort sap made to make controls adaptive to different form factors delivered a lot of value to developers.

And that was amplified by support of Kapsel plugins that allowed implementing out of the box basic/sso authentication, app logging and even offline odata.

Even now with all those native sdks or mdk stuff I am sure nothing can beat ui5+cordova in terms of value-to-effort.

And what’s more is unlike with mdk apps, I deal with “real” runtime, and not some abstraction level above nativescript which is managed by sap. This of course gives me freedom and flexibility.

You might be aware that SAP does not even mention this way of creating mobile apps in new courses or presentations.

I guess, this is because hybrid app approach is indeed dying,

So, most probably, it was too expensive for sap to support that direction with all the twists and turns in platforms development, like cordova-ios wkwebview issues.

The good news though is it is not going anywhere immediately.

Not sure what is on the Kapsel side, but the Apache Cordova itself is there, cordova/phonegap plugins are working, and you can continue doing the sapui5/openui5 apps at least with vanilla cordova.

WKWebView on ios

I am not going to go deep in details regarding the switch to the WKWebView itself, but the impact was going to be huge.

And it seemed huge indeed with $.ajax requests not working or files not loading from file:// scheme

You might have seen the Ludo Noens post about switching to the WKWebView with cordova-ios version prior to 6.0.0.

Also there was this howto at Cordova site which is now updated to contain changes after the 6.0.0 was relased.

So, the main thing there was introduction of scheme/host preference (you add it to config.xml) like this:

<platform name="ios">
    <allow-navigation href="app://*" />
    <preference name="scheme" value="app" />
    <preference name="hostname" value="localhost" />
</platform>

And it starts using the app://localhost url scheme instead of file://localhost to load local files.

What that means is you don’t need to install any plugins doing native ajax calls or anything like that.

You also don’t need this preference: <preference name=”WKWebViewOnly” value=”true” />​

It perfectly worked for me with two small catches:

  • I needed to properly setup CORS headers – and thanks to my nginx proxy I was able to easily do that (just make sure to add always parameter to get proper cors headers for 4xx and 5xx responses)
  • I had to have a valid SSL certificate for my backend requests – also thanks to letsencrypt and nginx+certbot it was a piece of cake to setup

And after doing that both of my apps successfully passed App Store processing/validation and were approved for beta testing in TestFlight.

Again, my apps don’t use Kapsel plugins, so I have no idea whether SAP was able to completely switch their uis/screens to WKWebView.

Overall Cordova Thoughts

Goodnight, sweet prince.

Use it in case you know what you’re doing and what your app development/maintenance strategy is in mid to long term.

 

Integration Cards

OK, now to this new shiny fiori 3 stuff called integration cards.

This was introduced a year ago, and you can find this blog by Daniel Vladinov that would give you an insight.

I actually overlooked it then, considering it something similar to OVP cards concept.

It turned out it was an effort to federate content from different systems as fiori 3 Central Home hype train states. Also see this video from fiori3 open sap course.

But what it has to do with our mobile apps?

Well, after browsing the belowed Explored section of the SDK in search of some calendar looking widget, I found this sample with what turned out to be Integration Cards Home Page.

So I decided to dig a little bit deeper into this direction to see, if I could use this layout in my openui5 app to make some widgets for main screen which I of course wanted to be adaptive at least for phone/tablet sizes.

And here’s what I came up with.

Important: As vizframe and timeline controls are part of sapui5 rather than openui5 dont expect those cards to work even though the integration.cards lib itself is included in openui5. Luckily for me, calendar card was there )

Important: Features described in this blog and code are relevant to ui5 version 1.80. As Integration Cards library seem to be under the development, some of the breaking changes and inconsistencies may be found in future.

Card types

First, be aware that there are sap.f.Card and sap.ui.integration.widgets.Card

Some more information can be found in sdk here

There are clear recommendations which to use when, namely sap does not recommend using integration cards “When you have to use an application model“, but we will disregard this, and I will show you why.

So, by card types we will further understand only the integration cards types, which are found in Card Explorer

We are going to talk a little bit about calendar, list, object and component types.

Also check out this architecture/overview page in Card Explorer to get some idea about concepts/entities involved.

Card configuration

As you can see from Card Explorer, each card has its own manifest that defines its type and other parameters in separate sap.card section.

Basically, all of the cards consist of header definition, and (optionally) of content and configuration definitions via respective sections in manifests’s “sap.card” section.

It is pretty much straightforward until it comes to data fetching.

Basically, the idea is that the card itself manages own data which makes sense, as again, in fiori 3 data can come from multiple sources.

In simple cases like the ones that are mostly present in Card Explorer there’s locally stored json data which cards display.

In the real life of course, the data must be retrieved from the actual backend system, maybe even through different authentication methods.

And that’s what we can have parameters and destinations for.

Here’s an example of a card in Card Explorer doing the request to some destination

So, as you can see in the example, there’s magic starting happening, that involves some runtime objects responsible for cards behavior

Enter Integration Host

Personally I find this concept very cool.

As the definition of “what card would like to display” is static, there must be something that would allow us to dynamically adjust to the environment.

For example, in our solution we use authentication on the backend side, so for cards that are querying “online” data we want to somehow provide authentication parameters.

Also we can adjust actual backend url depending on the enviromnent (like cordova or xsa webide).

So, for each card we would create a Host with corresponding actions and destination resolver.

It would look like this (I promise, I will show you cardsDstResolver a little bit later):

card.setHost(new Host({ actions:actions, resolveDestination: self.cardsDstResolver }));

So what are those actions and where they come from?

Well, host actions are the reason we decided to decline the sap suggestion of using the f.cards

What if I wanted to get the list of possible actions card can perform depending on the backend state (or network state itself)?

It turns out, we can do that, and what is better – we can dynamically change our host actions availability depending on that.

Now this is getting more interesting, but still does not looks like a complete picture.

What if I want something to dynamically decide what data I get instead of just resolving the backend urls for me?

Enter Card Extensions

This is a missing piece of our puzzle that was added in 1.75, but whats more important, in 1.79 there was an option data.extension.method added to make our Extension handle the data requests for us.

So, each of our cards declares own Extension file like this + the cards that want their Extension to fetch data for them, add this getData method reference

"sap.card": {
	"extension":"./cardExtension",
	"data": {
		"extension": {
			"method": "getData"
		}
	}
}

This is a powerful concept which perfectly serves our needs for offline support. // I used vanilla localStorage api to persist offline data.

And whats also convenient, it allows us to add even some more Host Actions to the card.

And of course custom Formatters!

Ok, so either card Extensions by themselves or something else must handle all that stuff regarding the state, urls, auth parameters and data retrieval mechanics?

AppMgr to rule them all

Ok, let’s start putting it all together.

First, lets point out one thing – the way dependency loading in amd/ui5 is done via sap.ui.define, means in case the required module (identified by module path) was already loaded, it (the .js file) is not being requested again, and instead the evaluated JS object (in its current state) is going to be returned.

What that means is in case we have some MyHopefullySingleton.js module which is referenced by each other object via the same path like “my/shared/singletonModule” we can indeed have a single instance of that module.

So, this is how our {AppMgr} works.

This guy serves as a central point for the whole app state and data fetching stuff (from cards perspective at least).

It is being initialized in the Component {init method}, so this way we are sure each other module like controller, card extension or  card component is guaranteed to have it in proper state.

It is the moment btw when we {resolve our urls} using {the app manifest entry}, so that later AppMgr helps cards to get the proper backend url via {cardsDstResolver} (remember this guy?). Of course, for cordova build we replace dataSource url with “remote” one in our manifest.json.

var odataUrl, restUrl;
...
return {
...
	resolveUrls:function(mainSvc){
		odataUrl=mainSvc.uri;
		restUrl=odataUrl.replace('odata','rest');
	},
...
	cardsDstResolver:function(dst){
		if (dst=='odata_srv') return odataUrl;
		if (dst=='rest_api') return restUrl;
	},
...
}

Also to share this between the two apps (coachApp and clientApp have the same copy of “shared” folde whic is for now manually copied) without changing paths, I declared {dedicated resourceRoot} ui5 bootstrap, so each module requires it by the path “ru/fitrepublic/shared/model/appMgr

So, each Card Extension that is responsible for data fetching, in turn requires AppMgr and proxies the getData call to some specific method of the App Manager.

App Manager then decides which state our app is in (online/offline) and returns the Promise object.

Full picture and card examples

Lets consider the cards init flow:

  1. As we already mentioned, the AppMgr is initialized during the Component init phase.
  2. In case we are already onboarded, {we either use offline init data or fetch new one} from the backend.
  3. The init data contains {cards and dynamic cards actions information} that we use {to render our cards}.
  4. There (in renderCards method) we provide cads with every piece of runtime information/behaviour it would need, like {auth parameters}, host actions, or layout data (for some reason configurable on the backend side 🙂 ).
  5. As each card is being initialized from its manifest file, it requires an Extension (which in turn requires AppMgr in case it need something from it later) where some additional stuff like custom Formatters and Host Actions are being adjusted.
  6. In case card in online only, it resolves data request parameters and fetches data itself. Otherwise Extension calls AppMgr’s method to get the data.
  7. In case card type is “Component” some init magic happens in onCardReady method in Card’s Component.js

This applies to each card we can possibly have in our Main screen, and this way we ensure to have a uniform way of dealing with them.

Here are some examples of card types I mentioned earlier:

Profile (Object) card

{This card} represents our profile information, which must be available if we are offline.

So, the card uses the Extension.getData method which calls {AppMgr.getProfile()}

Calendar card

{This card} represents our scheduled events, and also must have offline support.

It also can have dynamic parameters actions, such as “Navigate to next workout” in case {there is at least one planned event ahead in time} (not the case in the screenshot though 🙂 ).

Static parameters action “Add Workout” available only for client can be seen on the screenshot

Unfortunately the card did not provide a way to fire an action by clicking the scheduled item, so I applied a {dirty hack} which is almost 100% guaranteed to break in the future.

Also some features I would expect to work, like {setting the first day of week} officially do not work here.

Issue: Sometimes (I have not yet found a pattern) appointments are not shown in the list below the calendar.

Purchases (List) Card

{This card} is online only and displays the result of odata request {done by the card itself}

It also uses some magic called filtering that allows user to apply some filters at runtime.

By clicking the item user is navigated to the Purchase page.

Issue: For some reason custom formatters were not called after changing the “filter”.

Search and Promo (Component) card

{This card} could be the f.card if we followed the guidelines. But instead it is a component card which means the card content section is handled by a {custom component} rather  than integration lib templates.

Here in {onCardReady} event we simply set the JSON model with some promo data retrieved via {AppMgr.getPromoData()} in case we are online.

Regular {sap.ui5.rootView} entry defines the initial view of or Component, so standard ui5 view/controller lifecycle logic happens after the card is initialized.

Search navigates us to other screen inside the “parent” app.

Chat messages (Component) card

{This card} is also of type component and it displays last three messages from the chat channel associated with the Purchase.

To be honest, chat implementation currently sucks as it uses odata channel instead of websockets, so users have to manually refresh the chat to get new messages. // You can find kind of working  example (i think i might have had some session init issues) of xsa+websockets in my other project: https://github.com/myakinkii/minesnf_xsa_demo

An interesting point here is that when at first I tried to override Component.init() method to have some backend request done before controller’s init method is triggered, that somehow broke the card init flow, and in the Controller getOwnerComponent returned undefined afterwards.

What worked was adding the backend call to {onCardReady} method which seem to be triggered after the card Compoment init is done (at least I hope so 🙂 ).

Of course, later in the Controller we do the init stuff {as follows} to set the first channel active and {query odata service} to get messages with $top operator.

 

MyMoney (List with no content) card

{This guy} is an example of a card with Numeric header and no content at all.

It serves as a kpi tile with aggregated value and navigation action to details.

The data is queried by card itself from the {MyMoney view} with group by clause on the cds side.

Again it took me a while to find out the “right” definition to make cds build accept it:

  view MyMoney as select from CoachBilling {
    key coach.id, sum(amount) as money:Integer, 0 as target:Integer
  } where state='P' group by coach.id;

Also the results are filtered as I showed earlier in cap analytical privileges subsection.

Issue: For some reason formatting the target value did not work for me as you can see in manifest.

Card Actions handling

In key takeaways I promised you we would have some loosely coupled widgets in our App.

And from the data fetching perspective it may look like we achieved that goal.

But once we have talked about some Host Actions, and the way we can define and adjust it, what is actually handling it?

Well, it turns out, cards can fire action events with some parameters.

The cool thing is we can add our custom attributes to those parameters.

So, both the card header and card content can have actions property defined.

For simlpe example we have Purchases card with {header action} firing with “static” arguments:

{
	"type": "Custom",
	"enabled": true,
	"parameters" :{ "dst":"purchases"}
}

and {items action} firing with arguments from the bound context:

{
	"type": "Custom",
	"enabled": true,
	"parameters" :{ "dst":"purchase","vars":{"id":"{id}"} }
}

Again, enabled (and visible properties) could be a function as we can see in {some Card Extensions}:

actions:[{
	type: "Custom",
	enabled: function(){ return AppMgr.getOnlineMode(); },
	parameters :{ "dst":"refresh"},
	icon: "sap-icon://refresh",
	text: AppMgr.geti18n("genericRefresh")
}]

Card action types are defined here in sdk.

Basically it is either direct link or custom handler // but there’s bizarre example of Submit + Showcard action which is a little bit too much for me.

So, our overall approach is simple: in renderCards method we have a {callback} to do the “basic” handling:

var pars=e.getParameter("parameters");
if (pars.dst=='refresh') {
	e.getParameter("card").refresh();
} else if(self.getOnlineMode()) {
	if (this[c.name+"ActionHandler"]) this[c.name+"ActionHandler"](pars);
	else this.getRouter().navTo(pars.dst,pars.vars);
} else this.showToast(this.geti18n('warningOfflineMode'));

What that means is in case we are offline we do nothing and show warning as in most cases we cannot get any reasonable result by navigating somewhere.

Otherwise we can try to refresh the card, navigate to “dst” route defined in app {manifest}, or call a {custom handler in Main.controller} if it is provided.

So, we expect non-component cards to fire the action event as we defined it in the card manifest file, but what about component cards like Search and Promo?

Well, it turns out, we have an api for that, and all we need to do is just fire an event ourselves in {regular callback} method in card’s {controller}:

searchPress:function(e){
	var card=this.getOwnerComponent().getCard();
	card.fireAction({
		parameters:{
			dst:"search",
			vars:{ q:e.getParameter("query") }
		}
	});
}

As you can see, I tried to define some structure of an event parameters where “dst” would mean the destination of the supposed card action navigation, while vars would be the required parameters.

Overall Integration Cards Thoughts

With card Extension data mechanism, Host api, and custom Component cards, I really liked how it all got into the whole picture, though it took me some time to come up with this “architecture”

Hope this verbose section was at least readable, and I suggest you take a close look into the code.

 

Conclusion

CAP stack – imo can be used for PoC or presale projects but currently is definitely is not a complete product from sap, so not sure about productive usage.

Learning curve – moderate learning curve especially when have prior expertise in hana/nodejs/ui development, but some topics require investigation.

Documentation – website documentation lacking consistency and topics, there’s need to use additional materials like external videos or code examples.

Cordova and Hybrid apps – again, sadly the stuff seem to die, but I still see the value there. Not sure about starting big projects though.

Integration cards – are an interesting concept from the frontend development perspective, and maybe you can find a place yo use it (like I tried to).

 

Time spent on project up to date – two calendar months with net tracked time of ~200 hours.

From my overall personal experience, with some time and effort invested in the area, it is possible for the developer to create stuff like this in 3 to 4 two-week sprints.

 

Again, sorry if this is a little bit overwhelming for the reader, but hope he will find some value in reading through this.

No matter whether you have managed to read this or not, I thank you for you time.

Regards, Alex.

 

P.S.

I am currently actively looking for job/project opportunities in ui5/cordova/nodejs areas.

Any help regarding this would be hugely appreciated.

In case you have any information about such options please contact me via linkedin: Alex Myakinkii

And thanks again for reading )

Be the first to leave a comment
You must be Logged on to comment or reply to a post.