This is a guest post from Simon Grimm, Ionic Developer Expert and educator at the Ionic Academy. Simon also created the Practical Ionic book, a guide to building real world Ionic applications with Capacitor and Firebase.

In this tutorial we will build a simple Capacitor PWA with Ionic and Angular. We will integrate functionality to capture an image and share our current position using Capacitor plugins which work inside both native apps and PWAs!

Finally we will bring our PWA to the outside world by hosting it on Netlify, so we got a full trip from start to hosted PWA in one go.

Ionic App Setup

To get started, simply use the Ionic CLI to create a blank new project with Angular integration and Capacitor directly enabled:

// Install the Ionic CLI globally if needed
npm i -g @ionic/cli

// Start a blank new Ionic app
ionic start ionicPwa blank --type=angular --capacitor

This will create a new Ionic application that already comes with all configuration for Capacitor in place. In this tutorial we won’t get into building native apps, but you could easily build your app for iOS and Android from this codebase using Capacitor in the end as well!

We will use Capacitor 3.0 in this tutorial, so check the dependencies inside your package.json for @capacitor/core and @capacitor/cli and if they are still below v3, install the latest version like this:

npm install @capacitor/cli@next @capacitor/core@next

At the time writing Capacitor 3 was still in beta, but we’re living on the edge today!

Make Your Ionic App PWA Ready

The process of making an Angular app PWA ready is quite easy given the Angular schematics that automatically change all necessary parts of your project and inject a Service Worker in the right place:

// Install Angular CLI globally if needed
npm i -g @angular/cli

// Run the Angular schematic for PWAs
ng add @angular/pwa

If you now check out your updated app/app.module.ts you’ll see that the Service Worker will be injected into our app when built for production:

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(),
    AppRoutingModule,
    ServiceWorkerModule.register('ngsw-worker.js',
      { enabled: environment.production }
    )],
  providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
  bootstrap: [AppComponent],
})
export class AppModule { }

That means, we just need to make sure we run the correct build in the end before publishing our PWA – we’ll come back to this as it’s important for Netlify.

Adding Capacitor Plugins

Capacitor plugins allow us to use native APIs or the according Web API implementation if we are running as a PWA (or standard website).

That means, the same call/code to capture an image or get the user’s geolocation works across different devices, and triggers the right functionality depending on the current run environment.

A tiny change is that since version 3 of Capacitor, all plugins need to be installed separately and don’t exist inside the core package anymore. But that doesn’t make the process any more complicated as you will see.

Capturing Photos with PWA Elements

Yes, we can easily access the camera from within our PWa using Capacitor. The only problem is that there’s no decent web UI when capturing an image, and so we install another package called PWA Elements next to the camera plugin in our app now:

npm i @capacitor/camera

// Overlay for image capturing on the web
npm install @ionic/pwa-elements

To enable those elements, we need to import the defineCustomElements and call it inside our src/main.ts like this:

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

// Add the import
import { defineCustomElements } from '@ionic/pwa-elements/loader';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.log(err));

// Call the loader
defineCustomElements(window);

Now we can import everything we need from the package and call getPhoto() to capture an image and set the resulting webPath to a local variable.

Go ahead and change your home/home.page.ts to:

import { Component } from '@angular/core';
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {

  myImage = null;

  constructor() {}

  async takePicture() {
    const image = await Camera.getPhoto({
      quality: 90,
      allowEditing: true,
      resultType: CameraResultType.Uri,
      source: CameraSource.Camera
    });

    this.myImage = image.webPath;
  }
}

Finally we need a simple button and ion-img to display the captured image inside our app, so change the home/home.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Capacitor PWA
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-button (click)="takePicture()" expand="block">
    <ion-icon name="camera" slot="start"></ion-icon>
    Capture image
  </ion-button>

  <ion-img *ngIf="myImage" [src]="myImage"></ion-img>
</ion-content>

You can already try this inside your browser by bringing up the preview with ionic serve now!

Of course the image capturing only works if you have some kind of webcam attached or inside your computer.

Geolocation

To show that the usage of every Capacitor plugin is really that easy, let’s install two more plugins to get the current user location and natively share it:

// Install the geolocation plugin
npm i @capacitor/geolocation

// Install the share plugin
npm i @capacitor/share

Just like before we can directly import the necessary functions from the two packages to first grab the position by calling getCurrentPosition() and then store that value so we can share it later with the share plugin.

For this, bring up the home/home.page.ts again and add the new code:

import { Component } from '@angular/core';
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import { Geolocation, Position } from '@capacitor/geolocation';
import { Share } from '@capacitor/share';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {

  myImage: string = null;
  position: Position = null;

  constructor() {}

  async takePicture() { ... }

  async getCurrentPosition() {
    const coordinates = await Geolocation.getCurrentPosition();

    this.position = coordinates;
  }

  async share() {
    await Share.share({
      title: 'Come and find me',
      text: `Here's my current location: 
        ${this.position.coords.latitude}, 
        ${this.position.coords.longitude}`,
      url: 'http://ionicacademy.com/'
    });
  }
}

Now we need some additional buttons to trigger our new functionality and to display the result, which we can easily do inside an ion-card.

Open the home/home.page.html again and change it to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Capacitor PWA
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-button (click)="takePicture()" expand="block">
    <ion-icon name="camera" slot="start"></ion-icon>
    Capture image
  </ion-button>

  <ion-img *ngIf="myImage" [src]="myImage"></ion-img>

  <ion-button (click)="getCurrentPosition()" expand="block">
    <ion-icon name="locate" slot="start"></ion-icon>
    Get position
  </ion-button>

  <!-- present the geolocation information -->
  <ion-card *ngIf="position">
    <ion-card-content>
      <ion-item>
        <ion-icon name="location" slot="start"></ion-icon>
        Lat: {{ position.coords.latitude }}
      </ion-item>
      <ion-item>
        <ion-icon name="location" slot="start"></ion-icon>
        Lng: {{ position.coords.longitude }}
      </ion-item>

      <ion-button (click)="share()" expand="block" color="secondary">
        <ion-icon name="share" slot="start"></ion-icon>
        Share!
      </ion-button>
    </ion-card-content>
  </ion-card>
</ion-content>

With those two functionalities you might get into some trouble, because:

  • Only recent browser versions support the new Web Share API
  • Safari doesn’t capture a location on unsecure URLs (http), which localhost normally is

That means, we really need to run our PWA on a real device soon. But before, a quick recommendation for testing your PWA locally in general.

Testing your Capacitor PWA

While developing your app, the Service Worker is not injected unless you make a production build. That means, when you run the standard ionic serve command it’s not a full PWA yet.

If you want to test out the different PWA functionalities locally I recommend to simply install the http-server package, which can then host your application.

Once installed, you just need to create a production build and run the local server like:

ionic build --prod
http-server www

This won’t change the previously mentioned problems of browser version or security, but it’s something you need to know about building & testing your PWA anyway for the future.

But people from outside still can’t access localhost, right? So let’s release our Capacitor PWA to the world!

Deploying your Capacitor PWA

If you got your own server, you could simply copy the www folder you get after creating a build of your app and upload it there.

Since most of the time that’s not the case, we can fall back to a simple static hosting service like Netlify instead.

Because Netlify needs to build our app in the end, we need to add a new script to the scripts object of our package.json right now:

"build:prod": "ng build --prod"

This makes it easier to run a production build automatically.

For now, we need to head over to Github or Bitbucket (whatever you prefer) to create a repository over there which we can connect to Netlify afterwards.

I’ve used Github for this example and created a new repository without any files:

Now we need to upload our files to Github, and since Ionic already initialised a Git repository in our local project, we just need to add and commit our code.

Then, follow the instructions from Github/Bitbucket to connect your new repository, set it as the remote and push your code:

git add .
git commit -am 'Initial commit.'

git branch -M main

# Copy this from your repository!
git remote add origin https://github.com/saimon24/capacitor-pwa.git
git push -u origin main

Our code is inside the repository, now we can connect it to Netlify.

Inside your account, click on New site from Git to start the wizard that will add your project. You will need to authorise Netlify to access your Github (Bitbucket) account and you can select the previously created project.

The important part is now to configure the build correctly in the next step:

  • Build command: npm run build:prod
  • Publish directory: www

Based on this information, Netlify can now pull in your code, run a production build (using the additional script we added to the package.json!) and host the output www folder to serve our Capacitor PWA!

Once you deploy the site, you can see the log and finally get the URL to your deployment. The example PWA for this tutorial is right here.

If you want to test it correctly, add it to your home screen and start it from there.

The camera and geolocation plugin will automatically ask for permission, and you could use the same code inside a native app now as well!

Conclusion

With Capacitor, you can easily access native device functionalities inside your PWA or native app, and by adding a static site hosting to the mix you get a simple and reliable workflow for building hosting your Ionic PWA!

Instead of Angular, you could also use any other framework (or none at all) in combination with Capacitor as it’s rapidly becoming the framework of choice for anyone who wants to build web apps that run natively on iOS, Android, and the Web.

If you want to learn even more about Ionic and Capacitor with a library of 60+ video courses, templates and a supportive community, you can join the Ionic Academy and get access to a ton of learning material to boost your Ionic development skills.

And don’t forget to subscribe to my YouTube channel for fresh Ionic tutorials coming every week!

Signup for the Ionic Newsletter to get the latest news and updates!