How to wrap an Angular app with Apache Cordova

Recently, I have been investigating how I could wrap an Angular application into a native shell to extend the app with native capabilities and provide a richer user experience.
As a secondary goal, I aimed to avoid code duplication and ensure the same source code could serve web users as well as mobile users installing the natively wrapped version of it.
In terms of user experience, I intended to improve the navigation among internal and external URLs and improve the app loading time.

Screenshots of the wrapped Angular App with In-app links navigation and dialogs.

Introduction to Cordova

Cordova is a mature open-source project with a solid and active community. Cordova comes with a very simple and minimalistic core that can easily be extended by installing additional plugins. Also, Cordova has a super handy CLI to quickly bootstrap a new project and manage extensions.

In the next sections, I will briefly explain the core concepts of Cordova and how to set up a minimal project to wrap an Angular application.

Simply put, Cordova acts as a web server, serving web resources placed in the www folder. On top of that, Cordova provides an API accessible via a dedicated JS service:

<script type=”text/javascript” src=”cordova.js”></script>

The JS service acts as a bridge between the web application and the native wrapper, allowing, for example, the opening of specific URLs inside an in-app browser window.

<script type=”text/javascript”>
cordova.InAppBrowser.open(url,“_blank”);
</script>

Platforms and Plugins

Once an empty Cordova project has been created, platforms and plugins can be added. Adding platforms to the project will extend the support for additional platforms such as iOS, Android, etc. while adding plugins will extend the functionalities and provide ad-hoc native features.

Plugins and Platforms can be simply added using the Cordova CLI:

cordova platform add ios
cordova plugin add cordova-plugin-statusbar

Setting up a Cordova Project

Installing Apache Cordova

Installing Cordova on the local system is straightforward. This will also install the Cordova CLI that we will use to create a new project.

npm install -g cordova

Creating a new project

cordova create CordovaMobileApp com.acme.app "CordovaMobileApp"

Testing the Cordova project

The simplest method to test the newly created project is to add the browser platform and run the project.

cd CordovaMobileApp
cordova platform add browser
cordova run browser

The empty Cordova project comes with a default app skeleton, including an index.html file and an index.js script. More importantly, the project already includes the Cordova JS service to access the Cordova APIs.

Wrapping the Angular app — MVP setup

Wrapping an Angular app with Cordova is as simple as building the Angular app and placing it in the www Cordova’s project folder. 
I would advise keeping the Angular project and the Cordova project in two distinct folders to avoid mixing the respective node_modules folders and the project dependencies. When building the Angular app, the output-path option can be configured to place the build app directly into the Cordova’s www folder. Also, the base-href option should be used to set the base reference to “.” as absolute paths are not well handled by Cordova.

ng build --prod --base-href . --output-path ../CordovaMobileApp/www/

Adding the Cordova JS script

Lastly, to fully integrate Cordova, its script file needs to be referenced in the index.html file. This will ensure that the Cordova APIs are loaded and the “deviceready” event is fired.
Note: The script file, cordova.js, is automatically served by the Cordova wrapper; you don’t need to place it anywhere.

<script type=”text/javascript” src=”cordova.js”></script>

Integrating with the Cordova lifecycle

Cordova fires different events to signal the web app about the current lifecycle status; the most important ones are “deviceready”, “pause” and “resume.”

The Angular app should only be bootstrapped once the Cordova “deviceready” event is fired. This ensures that all resources have been loaded and the Cordova service API is ready to be called. To achieve this, it is sufficient to modify the Angular main.ts file as following:

let onDeviceReady = () => {
platformBrowserDynamic().bootstrapModule(AppModule);
};
document.addEventListener('deviceready', onDeviceReady, false);

Test on iOS

After adapting the Angular app to load the cordova.js file and waiting for the device-ready event before bootstrapping, we can then test the integration.

cordova platform add ios
cordova emulate ios

Advanced Integration

The following sections will cover a more advanced integration, which allows for a seamless connection of the Angular app with the Cordova API via an ad-hoc Angular Service.

We will use the service to make the Angular app aware of the surrounding environment, to know when the app is running on a browser or wrapped by Cordova. This is especially useful to avoid code duplications or the need to keep a separate code branch dedicated to Cordova. Also, the service will provide quick access to Cordova’s specific functions, e.g., hiding the app-loading splash screen or registering for Cordova events.

The Cordova Service

Building an injectable Angular service providing utility functions and a reference to the Cordova Javascript object.

ng g service Cordova

A minimal version of the Cordova service will allow us to ask the service if the app is running on Cordova, retrieve the JS Cordova object to access the APIs, and subscribe to the resume event. Listening to the resume event is particularly useful if the content of the app needs to be refreshed.

import { Injectable,NgZone } from ‘@angular/core’;
import { Subject } from 'rxjs/Subject';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/map';

function _window(): any {
// return the global native browser window object
return window;
}
@Injectable()
export class CordovaService {

private resume: BehaviorSubject<boolean>;
   constructor(private zone: NgZone) {
this.resume = new BehaviorSubject<boolean>(null);
Observable.fromEvent(document, 'resume').subscribe(event => {
this.zone.run(() => {
this.onResume();
});
});
}

get cordova(): any {
return _window().cordova;
}
   get onCordova(): Boolean {
return !!_window().cordova;
}
   public onResume(): void {
this.resume.next(true);
}
}

Registering and propagating lifecycle events

Cordova fires a resume event any time the application resumes from the background. In order to correctly process this event within the Angular application scope, NgZone is used to run the event listener inside the Angular zone.

constructor(private zone: NgZone) {
this.resume = new BehaviorSubject<boolean>(null);
Observable.fromEvent(document, 'resume').subscribe(event => {
this.zone.run(() => {
this.onResume();
});
});
}

In-App Browsing

A common use case for mobile apps is the need to open external links. Without wrapping the web application, those links are typically opened in a different tab of the browser. On Desktop, this may be a nice behavior; however, navigating among tabs on a mobile device is quite a miserable experience. Instead, an in-app browser allows the opening of external links within the same application and not forcing the user to leave the app.

This is achieved by installing the Cordova in-app browser plugin “cordova-plugin-inappbrowser” Personally, I do prefer an alternative plugin “cordova-plugin-safariviewcontroller” that makes uses of the modern SafariViewController (iOS) and Chrome Custom Tabs (Android) views to render in-app web pages.

cordova plugin add cordova-plugin-inappbrowser
cordova plugin add
cordova-plugin-safariviewcontroller

To open the external URLs, we can extend our Angular CordovaService to make use of the installed plugin.

public openLinkInBrowser(url: string) {
_window().SafariViewController.isAvailable(function(available) {
if (available) {
_window().SafariViewController.show({
url: url,
barColor: “#f7f7f9”,
tintColor: “#1ca8dd”,
controlTintColor: “#1ca8dd”,
});
} else {
_window().cordova.InAppBrowser.open(url,“_blank”);
}
})
}

Note that both plugins are required, since the SafariViewController may not be available on old devices.

Solving routing issues

Cordova has some issues in dealing with Angular default PathLocationStrategy as it does not seem to fully support HTML5 single page applications. Instead, the router should be instructed to use the HashLocationStrategy.

RouterModule.forRoot(appRoutes, { useHash: true})

Replace resources served via CDN

While using resources delivered via a CDN (e.g., fonts, CSS or JS framewokrs, etc.) is great for web applications, this does not apply to wrapped mobile apps, especially if those apps can be used offline. For wrapped apps to work offline, all required resources should be fetched locally. To achieve it, it is sufficient to download those resources, place them in the Angular assets folder, and change their reference URLs.

Disable user selection

In contrast to web browsers, most native applications do not allow the selection of the texts on the UI. To make our Angular wrapped app look more like a native application, I recommend disabling the text selection.

To disable user text selection, it is sufficient to add a custom CSS rule that applies to any HTML element except for the input fields.

* {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

input {
-webkit-user-select: auto !important;
-khtml-user-select: auto !important;
-moz-user-select: auto !important;
-ms-user-select: auto !important;
user-select: auto !important;
}

Cordova Hooks

Cordova hooks allow registering custom scripts to be called by the
Cordova build system.

By leveraging the Cordova hooks, we can apply changes to the Angular app that are relevant to the wrapped version only. The most obvious difference is the presence of the cordova.js script in the index.html file. In addition to that, we may have some resources served via CDN that are instead locally fetched.

In the following example, a script is used to switch the configuration within the index.html file by uncommenting the Cordova-specific part and commenting the web version.

<!-- web-version-config-on -->
<meta name="viewport" content="width=device-width">
<style>
@import url('//fonts.googleapis.com/cssfamily=Montserrat:100');
</style>
<!-- end-web-version-config-on -->
<!-- cordova-version-config-off
<meta name="viewport" content="width=device-width, user-sc...">
<script type="text/javascript" src="cordova.js"></script>
<link rel="stylesheet" href="assets/fonts/montserrat-font.css">
<style>
* {user-select: none;...}
input {user-select: auto !important;}
</style>
end-cordova-version-config-off -->

The script used to comment and uncommon sections of the index.html file should be placed inside the before_prepare Cordova’s hooks folder : /CordovaMobileApp/hooks/before_prepare/01_switch_donfiguration.js

As a result, when Cordova builds a new version, the script replaces the comments within the index.html file, enabling the Cordova-specific part and disabling the web one. This approach helps avoid code duplication and requires only a few deviations between the Cordova and the web version of the Angular app.

Conclusion

This article aims to help you get started by introducing Cordova and providing some critical adjustments required for Angular to play well in a natively wrapped environment.
If you feel the article is missing mentioning other essential aspects, please leave a comment below. I will improve the article accordingly.

p.s. If you found this article useful, please consider clapping it or sharing it, for others to be found.

References