Progressive Web Apps (PWAs) are nowadays more than just a buzz word, they are an awesome experience that combines the best of the web and apps. A combination of modern Web APIs such as service workers and an app shell architecture to deliver native app experiences to web applications.

In this tutorial, we’ll build a PWA by combining two different technologies OutSystems (World’s Leading Low-Code) and JavaScript. To contextualized the tutorial we will start with a short explanation about the technological concepts.

Low-Code and OutSystems

Low-Code, in a simplistic way, is the process of dragging and dropping visual blocks of existing code into a workflow to create applications. This technology is designed to serve business users as well as professional developers.

For business users or “citizen developers” OutSystems enables the creation of useful designed applications under the experienced guidance of IT professionals.

For skilled developers, it enables a faster pace of work and much better governance, which consequently allows those professionals to be more efficient and focused on solving business needs.

PWA

They can be described as offline-first mobile apps or mobile-optimized versions of web pages, similar to regular applications but excluding the inconvenience of having to be downloaded from an app store. The meaning of progressive comes from the concept that these applications remove the friction between the experience of the same application in web and mobile.

How did we do it?

So we started by creating a very simple traditional web application in OutSystems, with only one module and one screen with a few cards to display some text… nothing really special here.

After that, we created a manifest file ( the format/language of the file is JSON, the extension can be either .json or .manifest) that contains information like name, display type, language, the start URL, icons, etc. As you can see below, there are two icons in this file. These icons need to be added to your OutSystems application as resources and deployed to the target directory so that they can be referred from this file.

{

"dir": "ltr",

"lang": "en",

"name": "OutSystems PWA is cool!",

"scope": "/PWA",

"display": "standalone",

"start_url": "https://simoessalvador.outsystemscloud.com/PWA/Home.aspx",

"short_name": "OutSystems PWA",

"theme_color": "#039dfc",

"description": "How to convert a traditional web app into a progressive web app using OutSystems and open source",

"orientation": "any",

"background_color": "transparent",

"icons": [

{

"src": "/PWA/manifest-icon-192.png",

"sizes": "192x192",

"type": "image/png"

},

{

"src": "/PWA/manifest-icon-512.png",

"sizes": "512x512",

"type": "image/png"

}

],

"generated": "true"

}

The manifest file is primarily responsible for enabling the application to standalone, meaning it defines how it will be launched when it’s added to the home screen.

The next step is to generate and add the launching images and app icons, as resources, to the OutSystems application. These images will be used while your application is opening to increase the engagement with the user. We used a generator to create these images automatically; you can find more information about this generator, build by Önder Ceylan, here: https://itnext.io/pwa-splash-screen-and-icon-generator-a74ebb8a130

So by now, we have an OutSystems traditional web application with one screen, a few splash screen images, some icons and a manifest file with configurations.

Once we have all this in place, we can then inject this information into our HTML page head. The quickest way we found was by using the OutSystems action called “AddPostProcessingFilter” in the preparation of our screen and by searching for the tag “</title>” and replacing it with the following:

"</title>

<link rel='manifest' href='/PWA/Manifest.manifest'>



<link rel='apple-touch-icon' sizes='180x180' href='/PWA/apple-icon-180.png'>

<link rel='apple-touch-icon' sizes='167x167' href='/PWA/apple-icon-167.png'>

<link rel='apple-touch-icon' sizes='152x152' href='/PWA/apple-icon-152.png'>

<link rel='apple-touch-icon' sizes='120x120' href='/PWA/apple-icon-120.png'>



<meta name='apple-mobile-web-app-capable' content='yes'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-2048-2732.png'

media='(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-2732-2048.png'

media='(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-1668-2388.png'

media='(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-2388-1668.png'

media='(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-1668-2224.png'

media='(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-2224-1668.png'

media='(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-1536-2048.png'

media='(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-2048-1536.png'

media='(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-1242-2688.png'

media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-2688-1242.png'

media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-1125-2436.png'

media='(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-2436-1125.png'

media='(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-828-1792.png'

media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-1792-828.png'

media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-1242-2208.png'

media='(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-2208-1242.png'

media='(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-750-1334.png'

media='(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-1334-750.png'

media='(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-640-1136.png'

media='(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'>

<link rel='apple-touch-startup-image'

href='/PWA/apple-splash-1136-640.png'

media='(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'>"

With this step, we completed the standalone feature of our PWA.

BUT… a PWA is more than just a shortcut to your web app

For this same reason, we decided to implement a service worker to cache information and make our application to load faster. We decided to implement caching but we could have implemented any other Web API that is available on a service worker, the intention was to implement one as an example.

So we visited https://developers.google.com/web/tools/workbox/ and decided to implement the Cache First Strategy. We created a new js file for the service worker implementation with the following content:

importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.1.0/workbox-sw.js');



if (workbox) {

console.log(`Yay! Workbox is loaded`);



const shellHandler = new workbox.strategies.CacheFirst({

cacheName: 'shell',

plugins: [

new workbox.cacheableResponse.Plugin({

statuses: [0, 200]

}),

new workbox.expiration.Plugin({

maxEntries: 60,

maxAgeSeconds: 7 * 24 * 60 * 60, // 7 Days

}),

]

});



const imageHandler = new workbox.strategies.CacheFirst({

cacheName: 'images',

plugins: [

new workbox.expiration.Plugin({

maxEntries: 60,

maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days

}),

],

});



const fontHandler = new workbox.strategies.CacheFirst({

cacheName: 'font',

plugins: [

new workbox.expiration.Plugin({

maxEntries: 60,

maxAgeSeconds: 365 * 24 * 60 * 60, // 365 Days

}),

],

});



const htmlHandler = new workbox.strategies.StaleWhileRevalidate({

cacheName: 'index',

plugins: [

new workbox.expiration.Plugin({

maxEntries: 60,

maxAgeSeconds: 10 * 60 * 60, // 10 Minutes

}),

],

});



// image responses

workbox.routing.registerRoute(

/\.(?:png|gif|jpg|jpeg|svg)$/,

imageHandler,

);



// html responses

workbox.routing.registerRoute(

/.*(?:aspx|html|htm)$/,

htmlHandler,

);



workbox.precaching.precacheAndRoute([

'/PWA/Theme.PWA.extra.css',

'/PWA/Theme.PWA.css',

'/EPA_Taskbox/Blocks/EPA_Taskbox/Inbox_Flow/Inbox.css',

'/OutSystemsUIWeb/Theme.BaseTheme.css',

{ url: '/PWA/Home.aspx', revision: 'abcd1234' },

], {

// Ignore all URL parameters.

ignoreURLParametersMatching: [/.*/]

});



workbox.routing.registerRoute(

new RegExp('.*(Theme.FontAwesome.css|Icon.css)|.*.(woff|woff2|eot|ttf).*'),

fontHandler

);



} else {

console.log(`Boo! Workbox didn't load`);

}

There is a small but very important part of this script that we want to highlight; the CSS files must be included in the service workers caching strategy as part of the app shell caching, without this magic step the application will not be able to render as you expect.

Once our service worker file is ready, it can be uploaded, the same can be included in the resources of the application and deployed to the target directory as we did with the manifest file. This service worker file has to be registered to your app by calling navigator.serviceWorker.register. This can be done by adding the following script block in the HTML head section of the page:

<script type='text/javascript'>

if ('serviceWorker' in navigator) {

window.addEventListener('load', function() {

navigator.serviceWorker.register('/PWA/sw.js');

});

}

</script>

And voilá!

This experience was a combined work from Önder Ceylan and Ruben Bonito!

Demo: https://simoessalvador.outsystemscloud.com/PWA/Home.aspx