Build the Ionic Native wrapper for a Cordova plugin
In this post we will learn how to create the @awesome-cordova-plugins wrapper to make using Cordova plugins easier. This simplification is achieved by mapping Cordova plugin’s callback funtions to Promises and Observables.
@awesome-cordova-plugins was previously known as @ionic-native.
The project only changed its name, the functionality continues the same.
This post is the continuation of the following Cordova plugin tutorial:
So we will build the wrapper for the plugin of that tutorial.
You could find the source code of this tutorial on GitHub:
https://github.com/adrian-bueno/multiplatform-cordova-plugin-example (at packages > awesome-cordova-plugins-example).
We will start by defining how to approach this depending if you are building a public or private plugin. Afterwards we will see how to write the wrapper using TypeScript.
My plugin will be public
If your plugin will be public, just fork the official @awesome-cordova-plugins repository. Then clone your fork:
git clone https://github.com/<your-username>/awesome-cordova-plugins.git
Once its finished cloning, create a new directory with the name of your plugin inside src/@awesome-cordova-plugins/plugins
.
Then create a new file index.ts
. We will fulfill this file in the section: Pluggin mapper code.
When the code is ready, create a pull request to the official repository. After your pull request is acepted, your plugin wrapper will be available to download in npm
with the package name @awesome-cordova-plugins/<your-plugin-directory-name>
..
My plugin will be private
To build a private plugin wrapper we have 2 options:
-
Create a fork of the official repository. Delete all the plugins from
src/@awesome-cordova-plugins/plugins
and just create directories for all our plugin wrappers. Then upload the generated packages to our privatenpm
repository. -
(This is just a personal preference) Fork the official repository and adapt it to only contain one plugin wrapper. And delete all the files we don’t need.
Either way, we will have to keep the build scripts updated with the ones in the official repository.
I will show you how I adapted the official repository to only contain one plugin wrapper and reduce the repository code.
You could skip all this guide and just use my code on GitHub.
First, clone the official repository and change its name:
git clone --depth=1 https://github.com/danielsogl/awesome-cordova-plugins.git awesome-cordova-plugins-example
Delete the following directories and files:
π .git
π .github
π .husky
π docs
π scripts
βββ π docs
βββ π tasks
β βββ π publish.ts
βββ π templates
βββ π logger.ts **
π src
βββ π @awesome-cordova-plugins
π .editorconfig **
π .eslintignore **
π .eslintrc **
π .gitbook.yml
π .prettierignore **
π CHANGELOG.md **
π CODE_OF_CONDUCT.md
π DEVELOPER.md
π gulpfile.js
π prettier.config.js **
π LICENSE **
π renovate.json
π tsconfig.core.json
You could keep the files marked with
**
, but adapt them to your code and preferences. If you removelogger.ts
you would have to useconsole.log
in the files that use this logger.
Delete de
.git
directory and create a new repository.
Remove the paths
property from tsconfig.json
and edit the value from the include
property:
{
"compilerOptions": {
"baseUrl": ".",
"declaration": true,
"stripInternal": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"noImplicitAny": true,
"module": "es2015",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "src",
"target": "es5",
"skipLibCheck": true,
"lib": ["es2017", "dom"],
"inlineSources": true,
"inlineSourceMap": true
},
"include": ["src/**/*.ts"], <---
"angularCompilerOptions": {
"genDir": "aot"
}
}
Remove the paths
from scripts/tsconfig.json
too:
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"moduleResolution": "node",
"noImplicitAny": false,
"lib": ["es6"]
},
"exclude": ["node_modules"]
}
Clean the package.json
and install @awesome-cordova-plugins/core
as a dev dependency. Also change the name and version to your wrappers name:
{
"name": "awesome-cordova-plugins-example",
"version": "1.0.0",
"description": "Native plugin wrapper for cordova-plugin-example",
"scripts": {
"build:esm": "ts-node -P scripts/tsconfig.json scripts/tasks/build-esm",
"build:es5": "ts-node -P scripts/tsconfig.json scripts/tasks/build-es5",
"build:ngx": "ts-node -P scripts/tsconfig.json scripts/tasks/build-ngx",
"build": "npm run build:esm && npm run build:ngx && npm run build:es5",
"prebuild": "rimraf -rf dist"
},
"peerDependencies": {
"@awesome-cordova-plugins/core": "^5.1.0",
"rxjs": "^5.5.0 || ^6.5.0"
},
"devDependencies": {
"@angular/common": "11.2.14",
"@angular/compiler": "11.2.14",
"@angular/compiler-cli": "11.2.14",
"@angular/core": "11.2.14",
"@awesome-cordova-plugins/core": "^5.41.0",
"@types/cordova": "0.0.34",
"@types/fs-extra": "9.0.13",
"@types/lodash": "4.14.181",
"@types/node": "16.11.26",
"@types/rimraf": "3.0.2",
"@types/webpack": "5.28.0",
"fs-extra": "10.0.1",
"lodash": "4.17.21",
"rimraf": "3.0.2",
"rollup": "2.70.1",
"rxjs": "6.6.7",
"terser-webpack-plugin": "5.3.1",
"ts-node": "10.7.0",
"typescript": "4.1.6",
"unminified-webpack-plugin": "3.0.0",
"webpack": "5.71.0",
"winston": "3.7.2",
"zone.js": "0.11.5"
}
}
Now we have to replace in all the TypeScript files from the scripts
directory:
dist/@awesome-cordova-plugins/**
withnode_modules/@awesome-cordova-plugins/**
.src/@awesome-cordova-plugins/plugins
withsrc
.
We also have to change the variable PLUGIN_PATHS
from scrits/build/helpers.ts
:
export const PLUGIN_PATHS = [join(ROOT, 'src', 'index.ts')];
And remove transpileNgxCore()
from scripts/tasks/build-ngx.ts
, since we have delete the core module from src
.
Finally, create a new TypeScript script inside scripts/tasks
called build-packagejson.ts
:
import { readFileSync, writeFileSync } from 'fs-extra';
import path = require('path');
import { ROOT } from '../build/helpers';
const packageJson = JSON.parse(readFileSync(path.join(ROOT, "package.json")).toString());
const newPackageJson = {
name: packageJson.name,
version: packageJson.version,
module: packageJson.module,
main: packageJson.main,
peerDependencies: packageJson.peerDependencies
}
writeFileSync(path.join(ROOT, "dist/package.json"), JSON.stringify(newPackageJson, null, 4));
This scripts creates a new package.json
inside the dist
directory. It copies the package.json
from the root and ‘deletes’ all the not needed properties for the compiled wrapper. Add the properties you want to newPackageJson
.
Update the build
script from package.json
:
{
"build:packagejson": "ts-node -P scripts/tsconfig.json scripts/tasks/build-packagejson",
"build": "npm run build:esm && npm run build:ngx && npm run build:es5 && npm run build:packagejson",
}
Before running this scripts, we have to add our wrapper code in src/index.ts
.
Plugin mapper code
index.ts
(Open on GitHub)
import { Injectable } from '@angular/core';
import { Cordova, Plugin, AwesomeCordovaNativePlugin } from '@awesome-cordova-plugins/core';
import { Observable } from 'rxjs';
// We will define first two interfaces for
// the Bitcoin current price response.
// I don't know at this moment if we
// can define this interfaces in different files.
// I wasn't completely successful when I tried to
// do it, at least with the 'build-ngx' script.
export interface BPI {
code: string;
description: string;
rate: string;
rate_float: number;
symbol: string;
}
export interface BitcoinCurrentPriceResponse {
bpi: {
EUR: BPI;
GBP: BPI;
USD: BPI;
},
chartName: string;
disclaimer: string;
time: {
updated: string;
updatedISO: string;
updateduk: string;
}
}
// We need two decorators @Plugin and @Injectable.
//
// For the @Plugin properties, remember what we wrote
// in our plugin's plugin.xml file:
//
// <!-- www -->
// <js-module name="CordovaPluginExample" src="www/CordovaPluginExample.js">
// <clobbers target="cordova.plugins.CordovaPluginExample" />
// </js-module>
//
// pluginName == js-module.name
// pluginRef == clobbers.target
@Plugin({
pluginName: 'CordovaPluginExample',
plugin: 'cordova-plugin-example',
pluginRef: 'cordova.plugins.CordovaPluginExample',
repo: 'https://github.com/adrian-bueno/multiplatform-cordova-plugin-example',
platforms: ['Android', 'iOS', 'Electron']
})
@Injectable()
export class AwesomeCordovaPluginExample extends AwesomeCordovaNativePlugin {
// For every method, use the @Cordova decorator.
// Since in our Cordova plugin JavaScript file (www/CordovaPluginExample.js)
// we defined callback functions before parameters,
// now we have to declare 'callbackOrder' as 'reverse'.
@Cordova({
callbackOrder: 'reverse'
})
greeting(name?: string): Promise<string> { return null; }
// If a method can return multiple values over time,
// declare the property 'observable' as 'true'.
@Cordova({
observable: true,
callbackOrder: 'reverse'
})
countdownTimer(seconds?: number): Observable<number> { return null; }
// We don't have to add any logic to the methods, just return null.
@Cordova({
callbackOrder: 'reverse'
})
writeFile(fileName: string, text: string): Promise<string> { return null; }
@Cordova({
callbackOrder: 'reverse'
})
bitcoinCurrentPrice(): Promise<BitcoinCurrentPriceResponse> { return null; }
}
Check more @Plugin
and @Cordova
options (and other details) in the official developer documentation.
Test the wrapper in a Cordova app
This wrapper works with:
- Angular
- AngularJS
- React
- ES2015+/TypeScript
- VanillaJS
Read the official documentarion for more details and code examples.
I’m going to use an Angular (+ Cordova) app to test the wrapper.
# Install Angular CLI
npm i -g @angular/cli
# Create an Angular app
ng new <app-name>
cd <app-name>
Change Angular output path to www
and add ./
as the baseHref
in angular.json
:
{
...
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"baseHref": "./", <---
"outputPath": "www", <---
"index": "src/index.html",
"main": "src/main.ts",
...
}
Copy the config.xml
file we created in the main tutorial. You can copy it from my GitHub repository.
Now we can install the Cordova CLI and add the platforms we need:
# Install Cordova CLI
npm i -D cordova
# Create the www directory just
# to make Cordova not complaint
# while installing platforms
mkdir www
# Add Android platform
npx cordova platform add android
# Add iOS platform (only if you are using macOS)
npx cordova platform add ios
# Add Electron platform
npx cordova platform add electron@3.0.0
# Or if you want to use my cordova-electron fork
# to have the "keepCallback" functionality
npx cordova platform add https://github.com/adrian-bueno/cordova-electron#feature/keep-callback
Add
www
,plugins
andplatforms
to the.gitignore
file.
Install @awesome-cordova-plugins/core
:
npm i @awesome-cordova-plugins/core
Install the wrapper:
npm i <path-to-wrapper>/awesome-cordova-plugins-example/dist
Install the plugin:
npx cordova plugin add <path-to-plugin>/cordova-plugin-example
Tip: Move all the
dependencies
todevDependencies
(package.json) if you are building an Electron app. This will keep the output app smaller. Angular depencies are only needed in the compilation phase, but not while the app is running.
Add the cordova.js
script to index.html
:
<script src="cordova.js"></script>
Delete the default code from app.component.html
and app.component.ts
.
Add the wrapper class to the providers list in app.module.ts
:
// In Angular we must import the /ngx path
import { AwesomeCordovaPluginExample } from 'awesome-cordova-plugins-example/ngx';
@NgModule({
// ...
providers: [AwesomeCordovaPluginExample],
// ...
})
export class AppModule { }
If your plugin is public, its package name would be
@awesome-cordova-plugins/example
instead ofawesome-cordova-plugins-example
.
Now we are goint to create some buttons in AppComponent, like we did in the main plugin tutorial.
We will start with app.component.ts
:
import { Component } from '@angular/core';
import { AwesomeCordovaPluginExample } from 'awesome-cordova-plugins-example/ngx'
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
constructor(private awesomeCordovaPluginExample: AwesomeCordovaPluginExample) {}
greeting(name?: string) {
this.awesomeCordovaPluginExample.greeting(name)
.then(res => console.log(res))
.catch(error => console.error(error));
}
countdown(seconds: number) {
this.awesomeCordovaPluginExample.countdownTimer(seconds)
.subscribe(sec => console.log(sec));
}
writeFile(fileName: string, text: string) {
this.awesomeCordovaPluginExample.writeFile(fileName, text)
.then(res => console.log(res))
.catch(error => console.error(error));
}
bitcoinCurrentPrice() {
this.awesomeCordovaPluginExample.bitcoinCurrentPrice()
.then(res => console.log(res))
.catch(error => console.error(error));
}
}
app.component.html
:
<button (click)="greeting('World!')">
Greeting "Hello World!"
</button>
<button (click)="greeting()">
Greeting "Hello!"
</button>
<hr>
<button (click)="countdown(12)">
Countdown (12)
</button>
<hr>
<button (click)="writeFile('cordova-plugin-example.txt', 'Hello there π')">
Write file
</button>
<hr>
<button (click)="bitcoinCurrentPrice()">
Bitcoin current price
</button>
Our test app is ready. Now we are going to compile the Angular app and run it in a Cordova platform.
npm run build && npx cordova run android
npm run build && npx cordova run ios
npm run build && npx cordova run electron
Note: You could not run
cordova-electron@3.0.0
with Node.js 16, use version 14.
You should see the plugin results in the console after clicking a button. Read more details on how you could see Android and iOS app logs in the main post.