Quantcast
Channel: Devdactic – Ionic Tutorials
Viewing all 183 articles
Browse latest View live

How to Build an Ionic 4 File Explorer

$
0
0

Working with files in Ionic and Cordova applications can be painful and sometimes complicated, so today we want to go all in on the topic!

In this tutorial we will build a full file explorer with Ionic 4.

ionic-4-file-explorer

We’ll implement the basic functionalities to create and delete files and folders, and also implement an intelligent navigation to create a tree of folders to navigate around.

Setup Our Ionic File Explorer

To get started we just need a blank new app and 2 additional packages:

Make sure you install both the npm packages and also the cordova plugin:

ionic start devdacticExplorer blank
cd ./devdacticExplorer
npm install @ionic-native/file @ionic-native/file-opener
ionic cordova plugin add cordova-plugin-file
ionic cordova plugin add cordova-plugin-file-opener2

To use all of this also makre sure to add both packages to your app/app.module.ts like this:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { File } from '@ionic-native/file/ngx';
import { FileOpener } from '@ionic-native/file-opener/ngx';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    File,
    FileOpener
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now to one of the cool things of our Ionic file explorer: We will actually use only one single page, but reuse it so it works for all levels of our directory structure!

To do so, we can simply create another routing entry which also uses the default page, but with a different path that will contain a folder value that indicates in which folder we currently are.

Therefore change the app/app-routing.module.ts and notice the usage of the Angular 8 syntax for loading our module:

import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', loadChildren: () => import('./home/home.module').then( m => m.HomePageModule)},
  { path: 'home/:folder', loadChildren: () => import('./home/home.module').then( m => m.HomePageModule)},
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Now we got all the basic things in place, and because we basically only use Cordova plugins I highly recommend you start the app on a connected device using livereload by running the command like this:

ionic cordova run ios --consolelogs --livereload --address=0.0.0.0

All of our plugins won’t work inside the browser, and having the app on a device with livereload is your best bet to develop an app that makes heavy use of native functionality!

The Full Explorer View – In One Page

We could go about this one by one, but the changes we would have to apply along the way would be actually more confusing so let’s do the view in one take.

The main part of the view consists of an iteration over all the entries we find in a directory – be it files or folders. All entries have a click event, and can swipe in either the delete button or a copy/move button that starts a copy operation.

Talking of copy & move, we will simply select a first file which then sets a copyFile variable as a reference which file should be moved. We are then in sort of a transition phase, and in that phase also change the color of our toolbar.

Also, we will display either our generic title or the current path of the folder we navigated to. And if the current folder is not the root folder anymore, we also show the default back button so we can navigate one level up our directories again!

Now go ahead and change the app/home/home.page.html to:

<ion-header>
  <ion-toolbar [color]="copyFile ? 'secondary' : 'primary'">
    <ion-buttons slot="start" *ngIf="folder != ''">
      <ion-back-button></ion-back-button>
    </ion-buttons>
    <ion-title>
      {{ folder || 'Devdactic Explorer' }}
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-text color="medium" *ngIf="directories.length == 0" class="ion-padding ion-text-center">
    <p>No documents found</p>
  </ion-text>

  <ion-list>
    <ion-item-sliding *ngFor="let f of directories">
      <ion-item (click)="itemClicked(f)">
        <ion-icon name="folder" slot="start" *ngIf="f.isDirectory"></ion-icon>
        <ion-icon name="document" slot="start" *ngIf="!f.isDirectory"></ion-icon>
        <ion-label text-wrap>
          {{ f.name }}
          <p>{{ f.fullPath }}</p>
        </ion-label>
      </ion-item>

      <ion-item-options side="start" *ngIf="!f.isDirectory">
        <ion-item-option (click)="deleteFile(f)" color="danger">
          <ion-icon name="trash" slot="icon-only"></ion-icon>
        </ion-item-option>
      </ion-item-options>

      <ion-item-options side="end">
        <ion-item-option (click)="startCopy(f)" color="success">
          Copy
        </ion-item-option>
        <ion-item-option (click)="startCopy(f, true)" color="primary">
          Move
        </ion-item-option>
      </ion-item-options>

    </ion-item-sliding>
  </ion-list>

  <ion-fab vertical="bottom" horizontal="end" slot="fixed">
    <ion-fab-button>
      <ion-icon name="add"></ion-icon>
    </ion-fab-button>

    <ion-fab-list side="top">
      <ion-fab-button (click)="createFolder()">
        <ion-icon name="folder"></ion-icon>
      </ion-fab-button>
      <ion-fab-button (click)="createFile()">
        <ion-icon name="document"></ion-icon>
      </ion-fab-button>
    </ion-fab-list>
  </ion-fab>

</ion-content>

The fab list at the bottom reveals the two additional buttons to create a file or folder in the current directory, so nothing really special in there. Most of the stuff relies on the current directory you are in, and it all makes sense once we implement the real functionality now.

Listing Our Files and Folder

Now we gonna separate the functionality a bit since it would be too long for one snippet. First of all, we use the file plugin to load a list of directories. Because initially our folder is an empty string, it will use the basic this.file.dataDirectory (which is of course an empty list after installation).

We also implement the logic for retrieving the folder param from the paramMap of the activated route, which is appended to the directory that we list as well. This is the logic to list the different directories once we navigate to a next folder!

To start, simply change your app/home/home.page.ts to this so you also got already all imports that we need:

import { Component, OnInit } from '@angular/core';
import { File, Entry } from '@ionic-native/file/ngx';
import { Platform, AlertController, ToastController } from '@ionic/angular';
import { FileOpener } from '@ionic-native/file-opener/ngx';
import { Router, ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss']
})
export class HomePage implements OnInit {
  directories = [];
  folder = '';
  copyFile: Entry = null;
  shouldMove = false;

  constructor(
    private file: File,
    private plt: Platform,
    private alertCtrl: AlertController,
    private fileOpener: FileOpener,
    private router: Router,
    private route: ActivatedRoute,
    private toastCtrl: ToastController
  ) {}

  ngOnInit() {
    this.folder = this.route.snapshot.paramMap.get('folder') || '';
    this.loadDocuments();
  }

  loadDocuments() {
    this.plt.ready().then(() => {
      // Reset for later copy/move operations
      this.copyFile = null;
      this.shouldMove = false;

      this.file.listDir(this.file.dataDirectory, this.folder).then(res => {
        this.directories = res;
      });
    });
  }
}

The following will all take place in this file, simply append the functionality below the current functions.

Create new Folders and Files

We can trigger the two different actions with the fab buttons, and perhaps we could have even combined it into a single function. There is basically only a difference in the function we use, either createDir or writeFile from our file plugin.

For that function, we need to supply our current path (again, appending the folder to the root path) and then a name for the file or folder that we want to create.

Additionally we can also write content directly into the new file (you could also create an empty file), which works great if you download images from a server and write that blob data directly into a file!

Go ahead and append the following functions:

async createFolder() {
  let alert = await this.alertCtrl.create({
    header: 'Create folder',
    message: 'Please specify the name of the new folder',
    inputs: [
      {
        name: 'name',
        type: 'text',
        placeholder: 'MyDir'
      }
    ],
    buttons: [
      {
        text: 'Cancel',
        role: 'cancel',
        cssClass: 'secondary'
      },
      {
        text: 'Create',
        handler: data => {
          this.file
            .createDir(
              `${this.file.dataDirectory}/${this.folder}`,
              data.name,
              false
            )
            .then(res => {
              this.loadDocuments();
            });
        }
      }
    ]
  });

  await alert.present();
}

async createFile() {
  let alert = await this.alertCtrl.create({
    header: 'Create file',
    message: 'Please specify the name of the new file',
    inputs: [
      {
        name: 'name',
        type: 'text',
        placeholder: 'MyFile'
      }
    ],
    buttons: [
      {
        text: 'Cancel',
        role: 'cancel',
        cssClass: 'secondary'
      },
      {
        text: 'Create',
        handler: data => {
          this.file
            .writeFile(
              `${this.file.dataDirectory}/${this.folder}`,
              `${data.name}.txt`,
              `My custom text - ${new Date().getTime()}`
            )
            .then(res => {
              this.loadDocuments();
            });
        }
      }
    ]
  });

  await alert.present();
}

Now you are already able to test the basic functionality of our Ionic file explorer. Go ahead and create some files and folders, but right now we are not yet able to navigate or perform our other operations.

Delete Files & Start Copy/Move Process

This part is pretty fast – for the deletion of a file or folder we just need the path to the object and the name of it, which we can easily get from the information that is initially returned for the directory and stored locally.

To perform a copy or move operation, we first need to select a file that we want to move. In this function, we simply save a reference to it so later when we select the new destination, we know what to copy. This also changes how our header looks, and a click on an item should then have a different effect.

The two functions go as well into our current file:

deleteFile(file: Entry) {
  let path = this.file.dataDirectory + this.folder;
  this.file.removeFile(path, file.name).then(() => {
    this.loadDocuments();
  });
}

startCopy(file: Entry, moveFile = false) {
  this.copyFile = file;
  this.shouldMove = moveFile;
}

Now there is just one more piece missing…

Open Files, Perform Copy & Move Operations

The click event on an item can mean a few things, based on different conditions:

  • If it’s a file, we can open it using the second package we installed in the beginngin
  • If it’s a folder, we want to navigate into the folder by using the current path, appending the folder name and encoding everything so we don’t mess up the URL with additional slashes
  • If we selected a file for copy/move before, the now selected object needs to be a folder to which we can copy the file and finish our operation

This logic is reflected by the first function below, and the second one looks kinda strange but is just an if/else of two different conditions.

Either we want to move a file or copy it, and either it’s a directory or a file.

That’s why the function is pretty long, but as you can see it’s only a change of the function that you use from the file plugin!

async itemClicked(file: Entry) {
  if (this.copyFile) {
    // Copy is in action!
    if (!file.isDirectory) {
      let toast = await this.toastCtrl.create({
        message: 'Please select a folder for your operation'
      });
      await toast.present();
      return;
    }
    // Finish the ongoing operation
    this.finishCopyFile(file);
  } else {
    // Open the file or folder
    if (file.isFile) {
      this.fileOpener.open(file.nativeURL, 'text/plain');
    } else {
      let pathToOpen =
        this.folder != '' ? this.folder + '/' + file.name : file.name;
      let folder = encodeURIComponent(pathToOpen);
      this.router.navigateByUrl(`/home/${folder}`);
    }
  }
}

finishCopyFile(file: Entry) {
  let path = this.file.dataDirectory + this.folder;
  let newPath = this.file.dataDirectory + this.folder + '/' + file.name;

  if (this.shouldMove) {
    if (this.copyFile.isDirectory) {
      this.file
        .moveDir(path, this.copyFile.name, newPath, this.copyFile.name)
        .then(() => {
          this.loadDocuments();
        });
    } else {
      this.file
        .moveFile(path, this.copyFile.name, newPath, this.copyFile.name)
        .then(() => {
          this.loadDocuments();
        });
    }
  } else {
    if (this.copyFile.isDirectory) {
      this.file
        .copyDir(path, this.copyFile.name, newPath, this.copyFile.name)
        .then(() => {
          this.loadDocuments();
        });
    } else {
      this.file
        .copyFile(path, this.copyFile.name, newPath, this.copyFile.name)
        .then(() => {
          this.loadDocuments();
        });
    }
  }
}

Now with this logic in place, your Ionic file explorer is fully functional!

Conclusion

The trickiest element when working with files in Ionic can be the native path, which is not always working as expected or doesn’t show images for example.

Hopefully this file explorer gives you a good overview about what you could do with the underlying file system inside your Ionic app, including the reuse logic for the working path in our app!

You can also find a video version of this tutorial below.

The post How to Build an Ionic 4 File Explorer appeared first on Devdactic.


Building an Ionic 4 JWT Login with Tab Bar & Angular Routing

$
0
0

Building a JWT authentication flow is one of the basic things most apps have these days, but there are tricky elements that can make or break your app.

Today we will dive into the creation of an Ionic JWT app that allows us to login and protect our pages even when accessed as a URL in the browser. We also wanna have a tab bar routing, which is guarded by a login page upfront.

ionic-4-jwt-authentication

We’ll not create a backend but mock some data in the right place, but you can basically copy the whole approach from here into your project and just add your API URL in the right places.

Also, we’ll not make any authenticated requests in here and only focus on the autentication flow in our app. For more examples and courses on this topic check out the Ionic Academy.

Setting up our Ionic JWT Authentication App

First of all we start a new app but use the tabs template this time in order to save some time. If you don’t need tabs you can of course pick something else, most of the code would be the same.

We also need pages for our login and register which will be available to every user, and a service and guard that will hold all of our authentication logic.

Finally, the Ionic Storage package will be used to store our JWT and to decode it to retrieve its information. Now go ahead and run:

ionic start devdacticAuth tabs
cd ./devdacticAuth

ionic g page login
ionic g page register
ionic g service services/auth
ionic g guard guards/auth

npm i @ionic/storage @auth0/angular-jwt

In order to use our packages we also need to update our app/app.module.ts like this:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { IonicStorageModule } from '@ionic/storage';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule,
    IonicStorageModule.forRoot(),
    HttpClientModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now we are ready to store our token and make some dummy HTTP requests, let’s move on!

Changing the App Routing

The routing was always a huge problem (as I saw in many comments), so let’s make this as easy as possible.

We basically have three top level paths:

  • ”: Empty path, called in the beginning, displays the login page
  • ‘register’: Opens the registration page
  • ‘members’: Changed from ‘tabs’ to this to make the URL more pretty, basically the parent path of the tabs layout. The tabs actually have their own routing specified in another routing file in the tabs/ folder, we’ll get to that.

You can also already see that we apply our AuthGuard to the members path, which means everyone who wants to access a path starting with “members/” needs to be authenticated. So it can only be activated once the guard allows it, and we’ll implement that functionality soon.

In the code below we also use the new Angular 8 syntax for importing our pages, so go ahead and change your app/app-routing.module.ts to:

import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './guards/auth.guard';

const routes: Routes = [
  {
    path: 'members',
    loadChildren: () =>
      import('./tabs/tabs.module').then(m => m.TabsPageModule),
    canActivate: [AuthGuard]
  },
  {
    path: '',
    loadChildren: () =>
      import('./login/login.module').then(m => m.LoginPageModule)
  },
  {
    path: 'register',
    loadChildren: () =>
      import('./register/register.module').then(m => m.RegisterPageModule)
  }
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {}

As I said we also made a tiny change to the tabs routing, especially the names. This is just to show how you could change the elements in order to have a more polished URL, and automatically redirect to the first tab if we come here without a tab being selected (for example when someone would navigate only to “members/”.

For this, open your app/tabs/tabs.router.module and change it to:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TabsPage } from './tabs.page';

const routes: Routes = [
  {
    path: '',
    redirectTo: '/members/tab1',
    pathMatch: 'full'
  },
  {
    path: '',
    component: TabsPage,
    children: [
      {
        path: 'tab1',
        children: [
          {
            path: '',
            loadChildren: () =>
              import('../tab1/tab1.module').then(m => m.Tab1PageModule)
          }
        ]
      },
      {
        path: 'tab2',
        children: [
          {
            path: '',
            loadChildren: () =>
              import('../tab2/tab2.module').then(m => m.Tab2PageModule)
          }
        ]
      },
      {
        path: 'tab3',
        children: [
          {
            path: '',
            loadChildren: () =>
              import('../tab3/tab3.module').then(m => m.Tab3PageModule)
          }
        ]
      }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class TabsPageRoutingModule {}

Now we got the routing in place and you should be able to see the login, and if you navigate to http://localhost:8100/members/tab1 you should also still see your tab bar interface!

Now we just need to connect all of this with some logic.

Building the JWT Authentication Logic

The JWT authentication flow is the most important part in here, so we start with our service.

Let’s take a look at the different functions in detail:

loadStoredToken()

This function is meant to check your storage for a previously saved JWT. If we can find it, we decode the information and return that the user is already authenticated. All of this is wrapped into the user Observable.

We do this in order to use an Observable in the canActivate function of our guard in the next step.

The problem with previous examples was that initially a user is null when checking for it too early, which resulted in a flash of the login page and then showing the logged in pages.

We have to be careful to wait for the platform to be ready, then grab the token from storage. Both of these functions return a Promise, but with the help of the from() function we can convert them to Observables and easily use them in our pipe block.

PS: Using switchMap inside the pipe basically switches from one Observable to another!

login()

In here you would normally make a POST call to your backend and retrieve a JWT, but as I wanted to keep things simple we make a random API call and then simply change the result to a token I generated online.

We can then decode the token again and write it to the storage. Keep in mind that this write operation also returns a Promise, so we convert it to an Observable that we then return.

getUser()

This function will simply return the current value of our BehaviourSubject – the place where we store the decoded JWT data!

logout()

On logout we remove the JWT from storage, set the userData to null and then use the Router to guide the user back to the login page.

All of this takes now place right inside our services/auth.service.ts:

import { Platform } from '@ionic/angular';
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage';
import { BehaviorSubject, Observable, from, of } from 'rxjs';
import { take, map, switchMap } from 'rxjs/operators';
import { JwtHelperService } from "@auth0/angular-jwt";
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';

const helper = new JwtHelperService();
const TOKEN_KEY = 'jwt-token';
 
@Injectable({
  providedIn: 'root'
})
export class AuthService {
  public user: Observable<any>;
  private userData = new BehaviorSubject(null);

  constructor(private storage: Storage, private http: HttpClient, private plt: Platform, private router: Router) { 
    this.loadStoredToken();  
  }

  loadStoredToken() {
    let platformObs = from(this.plt.ready());

    this.user = platformObs.pipe(
      switchMap(() => {
        return from(this.storage.get(TOKEN_KEY));
      }),
      map(token => {
        if (token) {
          let decoded = helper.decodeToken(token); 
          this.userData.next(decoded);
          return true;
        } else {
          return null;
        }
      })
    );
  }

  login(credentials: {email: string, pw: string }) {
    // Normally make a POST request to your APi with your login credentials
    if (credentials.email != 'saimon@devdactic.com' || credentials.pw != '123') {
      return of(null);
    }

    return this.http.get('https://randomuser.me/api/').pipe(
      take(1),
      map(res => {
        // Extract the JWT, here we just fake it
        return `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE1Njc2NjU3MDYsImV4cCI6MTU5OTIwMTcwNiwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoiMTIzNDUiLCJmaXJzdF9uYW1lIjoiU2ltb24iLCJsYXN0X25hbWUiOiJHcmltbSIsImVtYWlsIjoic2FpbW9uQGRldmRhY3RpYy5jb20ifQ.4LZTaUxsX2oXpWN6nrSScFXeBNZVEyuPxcOkbbDVZ5U`;
      }),
      switchMap(token => {
        let decoded = helper.decodeToken(token);
        this.userData.next(decoded);

        let storageObs = from(this.storage.set(TOKEN_KEY, token));
        return storageObs;
      })
    );
  }

  getUser() {
    return this.userData.getValue();
  }

  logout() {
    this.storage.remove(TOKEN_KEY).then(() => {
      this.router.navigateByUrl('/');
      this.userData.next(null);
    });
  }
 
}

With the service in place we can use its functionality in our guard to check if a user is authenticated and therefore allowed to access a protected page.

As said before this function can actually return a lot of different things, one being an Observable. On this way we can truly wait for all the platform and storage stuff and make sure we return if we found a user token or not.

If the user is not allowed we will present an alert and guide him back to the login, otherwise we can return true which signals the router that the page should be displayed.

Go ahead with your guards/auth.guard.ts and change it to:

import { AuthService } from './../services/auth.service';
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { take, map } from 'rxjs/operators';
import { AlertController } from '@ionic/angular';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate{
  constructor(private router: Router, private auth: AuthService, private alertCtrl: AlertController) { }

  canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
    return this.auth.user.pipe(
      take(1),
      map(user => {
        if (!user) {
          this.alertCtrl.create({
            header: 'Unauthorized',
            message: 'You are not allowed to access that page.',
            buttons: ['OK']
          }).then(alert => alert.present());

          this.router.navigateByUrl('/');
          return false;
        } else {
          return true;
        }
      })
    )
  }
}

Ok that’s enough of RxJS logic for one tutorial, things get a lot easier from now on, I promise (pun intended).

Creating the Public & Protected App Pages

The login page needs just two buttons and two input fields for now and is fairly simple compared to everything before, so simple go ahead and change your app/login/login.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>Devdactic Auth</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content class="ion-padding">

  <ion-row>
    <ion-col size="12" size-sm="10" offset-sm="1" size-md="8" offset-md="2" size-lg="6" offset-lg="3" size-xl="4"
      offset-xl="4">

      <ion-card>
        <ion-card-header>
          <ion-card-title class="ion-text-center">Your Account</ion-card-title>
        </ion-card-header>

        <ion-card-content>

          <ion-item lines="none">
            <ion-label position="stacked">Email</ion-label>
            <ion-input type="email" placeholder="Email" name="email" [(ngModel)]="credentials.email"></ion-input>
          </ion-item>

          <ion-item lines="none">
            <ion-label position="stacked">Password</ion-label>
            <ion-input type="password" placeholder="Password" name="password" [(ngModel)]="credentials.pw">
            </ion-input>
          </ion-item>

          <ion-button (click)="login()" expand="block">Login</ion-button>
          <ion-button expand="block" color="secondary" routerLink="/register" routerDirection="forward">Register
          </ion-button>

        </ion-card-content>
      </ion-card>

    </ion-col>
  </ion-row>

</ion-content>

Nothing fancy, right?

Going to the register page already works through the routerLink we specified.

For the login we only need to call the function of our service and wait for the result – if it’s successful we route to the members path and otherwise simply display an alert!

There’s nothing else we need to do at this point as everything is happening in our service, so simply change your app/login/login.page.ts to:

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../services/auth.service';
import { AlertController } from '@ionic/angular';
import { Router } from '@angular/router';

@Component({
  selector: 'app-login',
  templateUrl: './login.page.html',
  styleUrls: ['./login.page.scss']
})
export class LoginPage implements OnInit {
  credentials = {
    email: 'saimon@devdactic.com',
    pw: '123'
  };

  constructor(
    private auth: AuthService,
    private router: Router,
    private alertCtrl: AlertController
  ) {}

  ngOnInit() {}

  login() {
    this.auth.login(this.credentials).subscribe(async res => {
      if (res) {
        this.router.navigateByUrl('/members');
      } else {
        const alert = await this.alertCtrl.create({
          header: 'Login Failed',
          message: 'Wrong credentials.',
          buttons: ['OK']
        });
        await alert.present();
      }
    });
  }

}

As a quick note, you also need to add a back button to the register page if you don’t want to get stuck on it. But for now we won’t implement all the register fields and logic – if you want more guidance and material on this just check out the Ionic Academy!

Therefore if you want to change your app/register/register.page.html to this:

<ion-header>
  <ion-toolbar color="secondary">
    <ion-buttons slot="start">
      <ion-back-button defaultHref="/"></ion-back-button>
    </ion-buttons>
    <ion-title>Register</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>

</ion-content>

Now your JWT login is working. THAT’S A SUCCESS!

But in order to show some data and complete the routing, there’s one more thing we need to do.

Showing User Data based on the JWT

We already decode the JWT information in our service, so let’s also make use of that data!

Because the service is extracting all the data before we even enter a secured page we can access it pretty safely now. We simply call the getUser() function which will return some data. And this is not an async operation because the service function is simply getting the current value of the userData BehaviourSubject!

Therefore, go ahead and change the app/tab2/tab2.page.ts to:

import { Component } from '@angular/core';
import { AuthService } from '../services/auth.service';

@Component({
  selector: 'app-tab2',
  templateUrl: 'tab2.page.html',
  styleUrls: ['tab2.page.scss']
})
export class Tab2Page {
  user = null;

  constructor(private auth: AuthService) {}

  ionViewWillEnter() {
    this.user = this.auth.getUser();
  }

  logout() {
    this.auth.logout();
  }
}

Also, the logout basically only happens in our service, we just need to call the right functionality.

Now the last part is to show some information from our decoded JWT and allow the user to logout in order to close the circle of our JWT authentication flow.

To do so, simply apply the following to your app/tab2/tab2.page.html:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Tab Two
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-card *ngIf="user">
    <ion-card-content>
      {{ user.email }} - {{ user.first_name }} {{ user.last_name }}
    </ion-card-content>
  </ion-card>

  <ion-button expand="block" (click)="logout()">Logout</ion-button>

</ion-content>

And that’s all we had to do to build a very robust JWT authentication for a tab based Ionic app!

Conclusion

If you understand the mechanism and respect how Promises and Observables work, it’s quite easy to chain them together.

Having race conditions of any kind in your app is no good idea, so always make sure there’s no possibility things can go wrong by chaining them accordingly.

Now this app isn’t finished, you need to add your own API call to get a JWT, but then you could fully integrate the JWT package we used to automatically sign all the future requests of your app with the stored JWT, something we also implemented before in this tutorial.

You can also find a video version of this tutorial below.

The post Building an Ionic 4 JWT Login with Tab Bar & Angular Routing appeared first on Devdactic.

How to Build a Shopping Cart with Ionic 4

$
0
0

When you are building a shopping app with Ionic there is no way around having a decent cart. And displaying a cart plus keeping track of all items can be challenging depending on your data.

In this tutorial we will implement a shopping cart which keeps track of our items, displays a list of all items and calculates the total amount.

ionic-4-shopping-cart

Oh and we will also animate part of our app to create an even richer user experience by using the simple but awesome animate.css.

This topic was actually selected by the community in our voting so here we go!

Starting our Shopping Cart

We start as always with a blank app and add another page that will be the modal of our cart and a service to manage all cart interaction in the background. Also we can directly install our animation package with npm, so go ahead and run:

ionic start shoppingCart blank
cd ./shoppingCart

ionic g page pages/cartModal
ionic g service services/cart

npm install animate.css

Because we will use the generated page as a modal and not just a regular page, we need to add its module to our main app module. If you skip this step you will later encounter a problem when presenting the modal, so go ahead and change your app/app.module.ts to:

import { CartModalPageModule } from './pages/cart-modal/cart-modal.module';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, CartModalPageModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

We also need to import the CSS of the animation library which we can do right in our app/global.scss, simply add the import after the already existing imports:

@import '~animate.css/animate.min.css';

.cart-modal {
  --height: 50%;
  --border-radius: 10px;
  padding: 25px;
}

Oh and while we are here, let’s leave some CSS for the modal that we will create to make it look smaller on our screen, just like a simple overlay. The app I had in mind was the German “pizza.de” which looks kinda like this:
pizza-app-example

Building the Cart Service

Before we dive into all the view elements we need to take of the engine of our app – the cart service.

You should always use a service to keep track of your data and perform operations in order to call it from all places and pages of your app. A page should simply handle the view clicks and be the layer between the view and the logic to handle all interaction.

Our service needs to keep track of all items added to the cart and provide functions to get the cart or all product items which you might normally get from an API.

In order to make it easier to get the current count of products we can use a BehaviourSubject like we did many times before. With this variable, all other pages can simply subscribe to the cartItemCount and automatically receive any updates without further logic! We just need to make sure the call next() on the variable whenever we change the cart.

The functions to change our cart are pretty self-explanatory: Add an item, reduce the item count or completely remove a stack of items.

The logic looks more complicated than it actually is, we just need to find the right item in our cart array and then work with the amount property to change the count and finally update the BehaviourSubject.

For now that’s enough, so go into your services/cart.service.ts and change it to:

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

export interface Product {
  id: number;
  name: string;
  price: number;
  amount: number;
}
@Injectable({
  providedIn: 'root'
})
export class CartService {
  data: Product[] = [
    { id: 0, name: 'Pizza Salami', price: 8.99, amount: 1 },
    { id: 1, name: 'Pizza Classic', price: 5.49, amount: 1 },
    { id: 2, name: 'Sliced Bread', price: 4.99, amount: 1 },
    { id: 3, name: 'Salad', price: 6.99, amount: 1 }
  ];

  private cart = [];
  private cartItemCount = new BehaviorSubject(0);

  constructor() {}

  getProducts() {
    return this.data;
  }

  getCart() {
    return this.cart;
  }

  getCartItemCount() {
    return this.cartItemCount;
  }

  addProduct(product) {
    let added = false;
    for (let p of this.cart) {
      if (p.id === product.id) {
        p.amount += 1;
        added = true;
        break;
      }
    }
    if (!added) {
      this.cart.push(product);
    }
    this.cartItemCount.next(this.cartItemCount.value + 1);
  }

  decreaseProduct(product) {
    for (let [index, p] of this.cart.entries()) {
      if (p.id === product.id) {
        p.amount -= 1;
        if (p.amount == 0) {
          this.cart.splice(index, 1);
        }
      }
    }
    this.cartItemCount.next(this.cartItemCount.value - 1);
  }

  removeProduct(product) {
    for (let [index, p] of this.cart.entries()) {
      if (p.id === product.id) {
        this.cartItemCount.next(this.cartItemCount.value - p.amount);
        this.cart.splice(index, 1);
      }
    }
  }
}

Now we have a powerful service in the background that we can inject into all of our pages to manage our cart.

Adding the Order Page

First of all we now need a page to display the products (that we get from the service) and let the user add these items.

We also grab a reference to the cart and the item count of the cart, and after doing it once in the beginning the page is basically set up. The addToCart will simply call the function of our service and that’s it – the cart count will be updated through the BehaviourSubject to which our view in the next step will be subscribed

When we open the cart we present the page we created in the beginning and also apply our custom CSS class to it. The additional logic in the dismiss is only for our animations, more on that in a second.

For now open your app/home/home.page.ts and change it to:

import { CartService } from './../services/cart.service';
import { Component, ViewChild, ElementRef } from '@angular/core';
import { ModalController } from '@ionic/angular';
import { CartModalPage } from '../pages/cart-modal/cart-modal.page';
import { BehaviorSubject } from 'rxjs';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss']
})
export class HomePage {
  cart = [];
  products = [];
  cartItemCount: BehaviorSubject<number>;

  @ViewChild('cart', {static: false, read: ElementRef})fab: ElementRef;

  constructor(private cartService: CartService, private modalCtrl: ModalController) {}

  ngOnInit() {
    this.products = this.cartService.getProducts();
    this.cart = this.cartService.getCart();
    this.cartItemCount = this.cartService.getCartItemCount();
  }

  addToCart(product) {
    this.cartService.addProduct(product);
    this.animateCSS('tada');
  }

  async openCart() {
    this.animateCSS('bounceOutLeft', true);

    let modal = await this.modalCtrl.create({
      component: CartModalPage,
      cssClass: 'cart-modal'
    });
    modal.onWillDismiss().then(() => {
      this.fab.nativeElement.classList.remove('animated', 'bounceOutLeft')
      this.animateCSS('bounceInLeft');
    });
    modal.present();
  }

  animateCSS(animationName, keepAnimated = false) {
    const node = this.fab.nativeElement;
    node.classList.add('animated', animationName)
    
    //https://github.com/daneden/animate.css
    function handleAnimationEnd() {
      if (!keepAnimated) {
        node.classList.remove('animated', animationName);
      }
      node.removeEventListener('animationend', handleAnimationEnd)
    }
    node.addEventListener('animationend', handleAnimationEnd)
  }
}

The only tricky thing in our page is applying animations, but with the help of the animateCSS() function we can easily add new animations to our cart element. You just need to make sure that you either remove the animation class that you add afterwards to trigger it again, or keep the class (like when we open the modal) so the item stays out of the view.

That’s why the openCart has some additional code to keep the cart item out of view and later fly it in back again!

This element is accessed as a ViewChild and is actually a FAB button that also displays the item count using the async pipe in our view.

Besides that our view consists of a simple list for our products and a button to add each element. Most of the code below is also for aligning the rows inside an item accordingly to achieve a somewhat acceptable presentation.

For an even cooler list of items also check out my dynamic slides app or a complete implementation of an accordion list with product items!

Right now simply change your app/home/home.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic Shopping
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-fab vertical="top" horizontal="end" slot="fixed">
    <ion-fab-button (click)="openCart()" #cart>
      <div class="cart-length">{{ cartItemCount | async }}</div>
      <ion-icon name="cart" class="cart-icon"></ion-icon>
    </ion-fab-button>
  </ion-fab>
  <ion-list>
    <ion-card *ngFor="let p of products">
      <ion-card-header>
        <ion-card-title>{{ p.name }}</ion-card-title>
      </ion-card-header>
      <ion-card-content>
        <ion-row class="ion-align-items-center">
          <ion-col size="8">
            <ion-label color="secondary">
              <b>{{ p.price | currency:'USD' }}</b>
            </ion-label>
          </ion-col>
          <ion-col size="4" class="ion-text-right">
            <ion-button fill="clear" (click)="addToCart(p)">
              <ion-icon name="add"></ion-icon>
            </ion-button>
          </ion-col>
        </ion-row>
      </ion-card-content>
    </ion-card>
  </ion-list>
</ion-content>

We are now able to add items to our cart, but the fab is a bit small and also the number is kinda in the wrong place. But we can easily tweak this by applying some custom CSS to the button, the icon and the label right inside our app/home/home.page.scss:

ion-fab-button {
  height: 70px;
  width: 70px;
}

.cart-icon {
  font-size: 50px;
}

.cart-length {
  color: var(--ion-color-primary);
  position: absolute;
  top: 18px;
  left: 25px;
  font-weight: 600;
  font-size: 1em;
  min-width: 25px;
  z-index: 10;
}

Now your whole view is functional so far and even the modal of our shopping cart opens, but there’s no list of items yet.

Creating our Cart Modal Page

The last missing piece, the calculation and list of all selected items before a user continues with the checkout.

Our modal should show all information (like you saw in the screenshot before) and allow to quickly change the amount of selected items. Therefore we need some functions, but basically all of them are just making the right calls to our service!

So heres the code for our app/pages/cart-modal/cart-modal.page.ts:

import { Product, CartService } from './../../services/cart.service';
import { Component, OnInit } from '@angular/core';
import { ModalController, AlertController } from '@ionic/angular';

@Component({
  selector: 'app-cart-modal',
  templateUrl: './cart-modal.page.html',
  styleUrls: ['./cart-modal.page.scss'],
})
export class CartModalPage implements OnInit {

  cart: Product[] = [];

  constructor(private cartService: CartService, private modalCtrl: ModalController, private alertCtrl: AlertController) { }

  ngOnInit() {
    this.cart = this.cartService.getCart();
  }

  decreaseCartItem(product) {
    this.cartService.decreaseProduct(product);
  }

  increaseCartItem(product) {
    this.cartService.addProduct(product);
  }

  removeCartItem(product) {
    this.cartService.removeProduct(product);
  }

  getTotal() {
    return this.cart.reduce((i, j) => i + j.price * j.amount, 0);
  }

  close() {
    this.modalCtrl.dismiss();
  }

  async checkout() {
    // Perfom PayPal or Stripe checkout process

    let alert = await this.alertCtrl.create({
      header: 'Thanks for your Order!',
      message: 'We will deliver your food as soon as possible',
      buttons: ['OK']
    });
    alert.present().then(() => {
      this.modalCtrl.dismiss();
    });
  }
}

As you can see, only the getTotal actually performs a calculation in here (which we could/should also move to the service actually) to iterate over all items in the cart and add up the amount times the price of each selected item.

Regarding the checkout, that’s actually another story for a full post. Some interesting material on this can be found here, but simply let me know if you would like to see Paypal/Stripe/Alipay integration with this shopping cart and we’ll add a part 2!

For now though let’s finish the modal by implementing the view. Again, a lot of code but most of it is styling and aligning the items correctly, so go ahead with your app/pages/cart-modal/cart-modal.page.html and change it to:

<ion-content fullscreen>

  <div class="ion-text-end">
    <ion-button (click)="close()" fill="clear" color="dark">
      <ion-icon name="close" slot="start"></ion-icon>
    </ion-button>
  </div>

  <div class="ion-padding">

    <ion-list>
      <ion-item *ngFor="let p of cart" class="ion-text-wrap">
        <ion-grid>
          <ion-row class="ion-align-items-center">
            <ion-col size="2" class="ion-align-self-center">
              <ion-button color="medium" fill="clear" (click)="decreaseCartItem(p)">
                <ion-icon name="remove-circle" slot="icon-only"></ion-icon>
              </ion-button>
            </ion-col>

            <ion-col size="1" class="ion-align-self-center">
              {{ p.amount }}
            </ion-col>

            <ion-col size="2" class="ion-align-self-center">
              <ion-button color="medium" fill="clear" (click)="increaseCartItem(p)">
                <ion-icon name="add-circle" slot="icon-only"></ion-icon>
              </ion-button>
            </ion-col>

            <ion-col size="2" offset="5">
              <ion-button color="medium" fill="clear" (click)="removeCartItem(p)">
                <ion-icon name="close-circle" slot="icon-only"></ion-icon>
              </ion-button>
            </ion-col>
          </ion-row>
          <ion-row>
            <ion-col size="9">
              <b>{{ p.name }}</b>
            </ion-col>
            <ion-col size="3" class="ion-text-end">
              {{ p.amount * p.price | currency:'USD' }}
            </ion-col>
          </ion-row>
        </ion-grid>
      </ion-item>
      <ion-item>
        <ion-grid>
          <ion-row>
            <ion-col size="9">
              Total:
            </ion-col>
            <ion-col size="3" class="ion-text-end">
              {{ getTotal() | currency:'USD' }}
            </ion-col>
          </ion-row>
        </ion-grid>
      </ion-item>
    </ion-list>

    <ion-button expand="full" (click)="checkout()">
      Checkout
    </ion-button>
  </div>

</ion-content>

And now your shopping cart app is ready and working, with cool animations.

Who said cross platform apps can’t look good?

Conclusion

Implementing a shopping cart in your Ionic 4 app is basically about developing the right functionality in a service to keep track of the cart. The view will only present the items, and using a BehaviourSubject makes it even easier to display changes if they only happen in one single place of your app!

Again, if you want more information about payments just let me know in the comments.

You can also find a video version of this tutorial below.

The post How to Build a Shopping Cart with Ionic 4 appeared first on Devdactic.

The Ionic 4 Media Files Guide (Images, Movies & Audio)

$
0
0

Working with files in Ionic has traditionally been one of the biggest challenge, since iOS and Android both handle them quite different.

Today we will try to conquer the field of media files which means images, movies and audio files. And we will capture them with our Ionic 4 app, store them inside the apps data directory and play it!

This was the second place in your content vote with so many votes that I just had to create the tutorial on it as well.

ionic-4-media-files

For iOS all of this works pretty smooth, for Android we have to add some workarounds here and there to make everything work.

Setting up Our Media Plugins

This tutorial makes heavy use of Cordova plugins as we gonna do a lot of different things with different media types. If you just need a specific feature pick the according package and Cordova plugin and only follow up with the code for it!

Also, of course make sure to test your app on a real device or emulator and not the browser – no chance with all of our plugins.

Now go ahead, get a coffee and let the installation for all the plugins run through:

ionic start devdacticMedia blank
cd ./devdacticMedia

npm i @ionic-native/media-capture
npm i @ionic-native/file
npm i @ionic-native/media
npm i @ionic-native/streaming-media
npm i @ionic-native/photo-viewer
npm i @ionic-native/image-picker

ionic cordova plugin add cordova-plugin-media-capture
ionic cordova plugin add cordova-plugin-file
ionic cordova plugin add cordova-plugin-media
ionic cordova plugin add cordova-plugin-streaming-media
ionic cordova plugin add com-sarriaroman-photoviewer
ionic cordova plugin add cordova-plugin-telerik-imagepicker

// For the imagepicker on Android we need a fix
cordova plugin add cordova-android-support-gradle-release
// https://github.com/wymsee/cordova-imagePicker/issues/212#issuecomment-438895540

Let’s see what each of these plugins does:

  • Media Capture: The plugin we use to capture videos, images and audio. Audio might be a problem on Android for some devices, so make sure you have a recorder app installed as well.
  • File: After capturing media you should move it to your apps folder, and with the File plugin we can do all of those operations
  • Media: To play Audio in an easy way, but there are also other alternatives to manage audio even better with additional controls
  • Streaming Media: Play a video in a nice player, but of course HTML5 video tags would work as well
  • Photo Viewer: We don’t want another component to show an image lightbox, so we can also use this plugin
  • Image Picker: As requested by the community, select multiple images from the library with this one

Now that we got all of this installed, you also need to add them to your app/app.module.ts so go ahead and change it to:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { ImagePicker } from '@ionic-native/image-picker/ngx';
import { File } from '@ionic-native/File/ngx';
import { MediaCapture } from '@ionic-native/media-capture/ngx';
import { Media } from '@ionic-native/media/ngx';
import { StreamingMedia } from '@ionic-native/streaming-media/ngx';
import { PhotoViewer } from '@ionic-native/photo-viewer/ngx';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    ImagePicker,
    MediaCapture,
    File,
    Media,
    StreamingMedia,
    PhotoViewer
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now the app is ready, but on iOS your app will likely crash because a lot of permissions are missing! To automatically include them with your build, you can add these entries in the iOS section of your config.xml:

<edit-config file="*-Info.plist" mode="merge" target="NSCameraUsageDescription">
    <string>need camera access to take pictures</string>
</edit-config>
<edit-config file="*-Info.plist" mode="merge" target="NSPhotoLibraryUsageDescription">
    <string>need photo library access to get pictures from there</string>
</edit-config>
<edit-config file="*-Info.plist" mode="merge" target="NSPhotoLibraryAddUsageDescription">
    <string>need to store photos to your library</string>
</edit-config>
<edit-config file="*-Info.plist" mode="merge" target="NSMicrophoneUsageDescription">
    <string>need to record your voice</string>
</edit-config>

Make sure to use a meaningful description and not just the ones above when releasing your app later on!

Loading our Media Files on Start

Basically all of our action will take place in one file, and therefore we will simply split the code a bit as one big snippet would get out of hand here.

For the beginning, let’s add all the imports and inject all the packages to our initial page.

Also, we take a bit different approach then we did in our complete guide to Ionic 4 images: This time we will not store a reference to the file, but simply list the content of a folder in our app!

Therefore, we have to check if the folder exists or otherwise create it, and then we can read the content into our files array.

So here’s the first part of our app/home/home.page.ts:

import { Component, OnInit } from '@angular/core';
import { ImagePicker } from '@ionic-native/image-picker/ngx';
import { ActionSheetController, Platform } from '@ionic/angular';
import {
  MediaCapture,
  MediaFile,
  CaptureError
} from '@ionic-native/media-capture/ngx';
import { File, FileEntry } from '@ionic-native/File/ngx';
import { Media, MediaObject } from '@ionic-native/media/ngx';
import { StreamingMedia } from '@ionic-native/streaming-media/ngx';
import { PhotoViewer } from '@ionic-native/photo-viewer/ngx';

const MEDIA_FOLDER_NAME = 'my_media';

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

  constructor(
    private imagePicker: ImagePicker,
    private mediaCapture: MediaCapture,
    private file: File,
    private media: Media,
    private streamingMedia: StreamingMedia,
    private photoViewer: PhotoViewer,
    private actionSheetController: ActionSheetController,
    private plt: Platform
  ) {}

  ngOnInit() {
    this.plt.ready().then(() => {
      let path = this.file.dataDirectory;
      this.file.checkDir(path, MEDIA_FOLDER_NAME).then(
        () => {
          this.loadFiles();
        },
        err => {
          this.file.createDir(path, MEDIA_FOLDER_NAME, false);
        }
      );
    });
  }

  loadFiles() {
    this.file.listDir(this.file.dataDirectory, MEDIA_FOLDER_NAME).then(
      res => {
        this.files = res;
      },
      err => console.log('error loading files: ', err)
    );
  }
}

As you can see most imports are still unused – we will use them now one by one.

Displaying Our Files

Before we dive into the more complex file stuff let’s quickly set up the view so we can actually see the result of our hard work.

From the previous step we have an array of files (specific: FileEntries) and we simply display them in our list with an according icon based on the file type.

Additionally we show the path but that’s just for us and the debugging, nothing important. Finally we add a button to the footer from which we will open a selection for the operation we want to perform.

So go ahead and change your app/home/home.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic Media Capture
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-list>
    <ion-item-sliding *ngFor="let f of files">
      <ion-item (click)="openFile(f)">
        <ion-icon name="image" slot="start" *ngIf="f.name.endsWith('jpg')"></ion-icon>
        <ion-icon name="videocam" slot="start" *ngIf="f.name.endsWith('MOV') || f.name.endsWith('mp4')"></ion-icon>
        <ion-icon name="mic" slot="start" *ngIf="f.name.endsWith('wav')"></ion-icon>

        <ion-label class="ion-text-wrap">
          {{ f.name }}
          <p>{{ f.fullPath }}</p>
        </ion-label>
      </ion-item>

      <ion-item-options side="start">
        <ion-item-option (click)="deleteFile(f)" color="danger">
          <ion-icon name="trash" slot="icon-only"></ion-icon>
        </ion-item-option>
      </ion-item-options>

    </ion-item-sliding>
  </ion-list>
</ion-content>

<ion-footer>
  <ion-toolbar color="primary">
    <ion-button fill="clear" expand="full" color="light" (click)="selectMedia()">
      <ion-icon slot="start" name="document"></ion-icon>
      Select Media
    </ion-button>
  </ion-toolbar>
</ion-footer>

There are also some functions connected to clicking on an item and removing it, but we’ll implement that later.

Capturing Images, Movies and Audio

Now it’s time to start with the first real part which is capturing files. We can present an action sheet and perform the according action after the user has picked an option.

All of the functions new make use of the Cordova plugins we installed in the beginning, and all of them will return a URL to the file on the device.

The media capture plugin always returns an array of result so we can always simply use the data[0].fullPath, and for the multiple selected images we can go through the array of results and perform our action for each of them.

Not really too hard stuff so open the app/home/home.page.ts again and append inside your class:

async selectMedia() {
    const actionSheet = await this.actionSheetController.create({
      header: 'What would you like to add?',
      buttons: [
        {
          text: 'Capture Image',
          handler: () => {
            this.captureImage();
          }
        },
        {
          text: 'Record Video',
          handler: () => {
            this.recordVideo();
          }
        },
        {
          text: 'Record Audio',
          handler: () => {
            this.recordAudio();
          }
        },
        {
          text: 'Load multiple',
          handler: () => {
            this.pickImages();
          }
        },
        {
          text: 'Cancel',
          role: 'cancel'
        }
      ]
    });
    await actionSheet.present();
  }

  pickImages() {
    this.imagePicker.getPictures({}).then(
      results => {
        for (var i = 0; i < results.length; i++) {
          this.copyFileToLocalDir(results[i]);
        }
      }
    );

    // If you get problems on Android, try to ask for Permission first
    // this.imagePicker.requestReadPermission().then(result => {
    //   console.log('requestReadPermission: ', result);
    //   this.selectMultiple();
    // });
  }

  captureImage() {
    this.mediaCapture.captureImage().then(
      (data: MediaFile[]) => {
        if (data.length > 0) {
          this.copyFileToLocalDir(data[0].fullPath);
        }
      },
      (err: CaptureError) => console.error(err)
    );
  }

  recordAudio() {
    this.mediaCapture.captureAudio().then(
      (data: MediaFile[]) => {
        if (data.length > 0) {
          this.copyFileToLocalDir(data[0].fullPath);
        }
      },
      (err: CaptureError) => console.error(err)
    );
  }

  recordVideo() {
    this.mediaCapture.captureVideo().then(
      (data: MediaFile[]) => {
        if (data.length > 0) {
          this.copyFileToLocalDir(data[0].fullPath);
        }
      },
      (err: CaptureError) => console.error(err)
    );
  }

All of the functions call our copyFileToLocalDir() with the path to the file, which we now have to implement.

Working with File Paths

The copy function will copy a file from any place into the “my_media” folder inside our apps data directory (check our constant MEDIA_FOLDER_NAME we declared at the top). This is a safe place to keep files, you shouldn’t rely on any files outside your app in a temp directory!

To move the file we need the path, the file name, a new path and new name. To construct all of this we split some values from the path and create a new “random” name by using the current date.

Also, if we enter the copy function with a URL like “/var/…/..” we should append it with “file://” to make the copy function work!

If you receive an error code check its meaning, most likely your initial path to the file is somehow wrong.

In general the copy function works really great, and you can see some more file action also in our File Explorer with Ionic 4 tutorial.

For now go ahead with these additions for our app/home/home.page.ts:

copyFileToLocalDir(fullPath) {
    let myPath = fullPath;
    // Make sure we copy from the right location
    if (fullPath.indexOf('file://') < 0) {
      myPath = 'file://' + fullPath;
    }

    const ext = myPath.split('.').pop();
    const d = Date.now();
    const newName = `${d}.${ext}`;

    const name = myPath.substr(myPath.lastIndexOf('/') + 1);
    const copyFrom = myPath.substr(0, myPath.lastIndexOf('/') + 1);
    const copyTo = this.file.dataDirectory + MEDIA_FOLDER_NAME;

    this.file.copyFile(copyFrom, name, copyTo, newName).then(
      success => {
        this.loadFiles();
      },
      error => {
        console.log('error: ', error);
      }
    );
  }

  openFile(f: FileEntry) {
    if (f.name.indexOf('.wav') > -1) {
      // We need to remove file:/// from the path for the audio plugin to work
      const path =  f.nativeURL.replace(/^file:\/\//, '');
      const audioFile: MediaObject = this.media.create(path);
      audioFile.play();
    } else if (f.name.indexOf('.MOV') > -1 || f.name.indexOf('.mp4') > -1) {
      // E.g: Use the Streaming Media plugin to play a video
      this.streamingMedia.playVideo(f.nativeURL);
    } else if (f.name.indexOf('.jpg') > -1) {
      // E.g: Use the Photoviewer to present an Image
      this.photoViewer.show(f.nativeURL, 'MY awesome image');
    }
  }

  deleteFile(f: FileEntry) {
    const path = f.nativeURL.substr(0, f.nativeURL.lastIndexOf('/') + 1);
    this.file.removeFile(path, f.name).then(() => {
      this.loadFiles();
    }, err => console.log('error remove: ', err));
  }

As you can see we now also got the openFile() and deleteFile() in here. Deletion is simple, just pass in the path and the file name and the file will be removed!

For opening a file you should check what type of file you have to handle. Also, this is a problematic area as iOS and Android handle these things quite different so some plugins work great on the one platform but not really on the other.

Also, some plugins don’t want to have the “file://” in the path and some need it, so this can really cause a lot of confusion when debugging it.

Implement the functionality you need one by one, check remote debugging for native error codes from all the plugins and add optional some error blocks to log out the errors that we haven’t caught in our code yet!

Conclusion

While it’s likely not going to be that smooth in your app, I hope this Ionic media files guide gave you a good starting point to capture different types of media, manage files in your application and also present them to the user.

When your app crashes or doesn’t perform the operation like you expect, try to find the log of the Cordova plugin – there is always a reason why it’s not working, and in general you can make all of these medai operations work in your Ionic app!

Hopefully this gives you confidence for your next project.

You can also find a video version of this tutorial below.

The post The Ionic 4 Media Files Guide (Images, Movies & Audio) appeared first on Devdactic.

How to Build a Canvas Painting App with Ionic 4

$
0
0

The canvas is a very mighty element, which you can use for all kind of functionalities ranging from image manipulation to creating a painting app.

Therefore we will today incorporate a canvas in our Ionic 4 app, and build a drawing app on top of it!

ionic-4-canvas-painting

We’ll add some different colours, set a background image, change the stroke width and finally implement the according functionality to export the canvas into a real image (or first base64) that you can then use for any other operation!

Starting our Canvas App

To start, we just need a blank application and install the base64toGallery plugin in case we want to export an image to the camera roll of the user, so go ahead and run:

ionic start devdacticCanvas blank --type=angular
cd ./devdacticCanvas

npm install @ionic-native/base64-to-gallery       
ionic cordova plugin add cordova-base64-to-gallery

If you don’t need the device functionality you can also skip the plugin and the next step, but otherwise you need to make sure that you add this snippet to your config.xml inside the ios block:

<config-file parent="NSPhotoLibraryAddUsageDescription" target="*-Info.plist">
  <string>need to store photos to your library</string>
</config-file>

Otherwise your app will crash on iOS because of missing permissions!

Also, we need to import the plugin inside our app/app.module.ts like always:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { Base64ToGallery } from '@ionic-native/base64-to-gallery/ngx';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    Base64ToGallery
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now it’s time to get real.

Creating the Canvas App View

Let’s start with the view today, as this is actually the easier part of our tutorial.

We need to implement a view with a canvas and some additional functionality which is the selection of a color, the size of the line width and the buttons to set a background image and export the final canvas view.

For the canvas we can grab the events and call our own functionality at different stages, and there’s a slight difference between viewing the app on the browser and on a device: On the browser, only the mouse events will be fired, on a device only the touch events!

In detail this means:

  • mousedown / touchstart: Set the initial coordinates to start drawing and enable drawing mode
  • mousemove / touchmove: We moved the cursor/finger so we need to start drawing
  • mouseup / touchend: End the drawing action

Also, we can disable the bounce effect of our view which can be kinda annoying on iOS if you want to draw by setting forceOverscroll to false on our ion-content element!

The color iteration might not make sense to you yet, but we’ll create the according array of colors soon.

For now go ahead with your app/home/home.page.html and change it to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic Canvas Drawing
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [forceOverscroll]="false">
  <ion-row>
    <ion-col *ngFor="let color of colors" [style.background]="color" class="color-block" tappable
      (click)="selectColor(color)"></ion-col>
  </ion-row>
  <ion-radio-group [(ngModel)]="selectedColor">
    <ion-row>
      <ion-col *ngFor="let color of colors" class="ion-text-center">
        <ion-radio [value]="color"></ion-radio>
      </ion-col>
    </ion-row>
  </ion-radio-group>

  <ion-range min="2" max="20" color="primary" [(ngModel)]="lineWidth">
    <ion-icon size="small" slot="start" name="brush"></ion-icon>
    <ion-icon slot="end" name="brush"></ion-icon>
  </ion-range>

  <canvas #imageCanvas (mousedown)="startDrawing($event)" (touchstart)="startDrawing($event)"
    (touchmove)="moved($event)" (mousemove)="moved($event)" (mouseup)="endDrawing()" (touchend)="endDrawing()"></canvas>

  <ion-button expand="full" (click)="setBackground()">
    <ion-icon slot="start" name="image"></ion-icon>
    Set Background Image
  </ion-button>

  <ion-button expand="full" (click)="exportCanvasImage()">
    <ion-icon slot="start" name="download"></ion-icon>
    Export Canvas Image
  </ion-button>
</ion-content>

We can also apply some easy CSS rules to make the canvas stand out, so simply change the app/home/home.page.scss to:

canvas {
  border: 1px solid rgb(187, 178, 178);
}

.color-block {
  height: 40px;
}

While you can use this canvas mechanism to capture a signature, there’s also another tutorial on exactly that case using another package made specifically for that:

Building a Signature Drawpad using Ionic

Basics of the Canvas Element

Now we can start our canvas manipulation, and first of all we need to access to it by accessing it as a viewchild. We also add some general properties to our class which are needed to store some coordinates and our colors and line width.

We didn’t set the size of the canvas, so right now it looks kinda ugly. But we can manipulate the size directly in the ngAfterViewInit and set it to the size of the current platform. But using some CSS would work as well.

When we start the drawing (remember, connected to the start event of the canvas) we capture the x and y coordinates so we can then in the next step draw a line from one point to another.

Right now we can also add some more basic functions like setting the color, ending the draw mode and finally also loading an image into the background of the canvas. For this make sure to add an image to your assets folder, which is what we use in our case.

Go ahead by changing the app/home/home.page.ts to:

import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { Platform, ToastController } from '@ionic/angular';
import { Base64ToGallery, Base64ToGalleryOptions } from '@ionic-native/base64-to-gallery/ngx';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss']
})
export class HomePage implements AfterViewInit {
  @ViewChild('imageCanvas', { static: false }) canvas: any;
  canvasElement: any;
  saveX: number;
  saveY: number;

  selectedColor = '#9e2956';
  colors = [ '#9e2956', '#c2281d', '#de722f', '#edbf4c', '#5db37e', '#459cde', '#4250ad', '#802fa3' ];

  drawing = false;
  lineWidth = 5;

  constructor(private plt: Platform, private base64ToGallery: Base64ToGallery, private toastCtrl: ToastController) {}

  ngAfterViewInit() {
    // Set the Canvas Element and its size
    this.canvasElement = this.canvas.nativeElement;
    this.canvasElement.width = this.plt.width() + '';
    this.canvasElement.height = 200;
  }

  startDrawing(ev) {
    this.drawing = true;
    var canvasPosition = this.canvasElement.getBoundingClientRect();

    this.saveX = ev.pageX - canvasPosition.x;
    this.saveY = ev.pageY - canvasPosition.y;
  }

  endDrawing() {
    this.drawing = false;
  }

  selectColor(color) {
    this.selectedColor = color;
  }

  setBackground() {
    var background = new Image();
    background.src = './assets/code.png';
    let ctx = this.canvasElement.getContext('2d');

    background.onload = () => {
      ctx.drawImage(background,0,0, this.canvasElement.width, this.canvasElement.height);   
    }
  }
}

You can of course use any other image, even from a different source. Just keep in mind that you are basically drawing the image on the canvas, so whatever was in it before is below the image, and everything that follows will be above!

Drawing on the Canvas Element

Now it’s time to perform our drawing action. We have created all the necessary things upfront, so right now this is a simple calculation to draw a line with a slected width and color.

To calculate the right positions you also have to count int the real position of the canvas on the screen, otherwise the points you are painting are at the wrong position due to the offset inside the view.

Performing operations on the canvas involves grabbing the current context, beginning a path from the previous spot to the current spot and then calling stroke().

By running this function over and over again whenever the mouse/touch moves we can create a nice line on our canvas, so again open the app/home/home.page.ts and append the following snippet below the already existing functionality:

moved(ev) {
  if (!this.drawing) return;

  var canvasPosition = this.canvasElement.getBoundingClientRect();
  let ctx = this.canvasElement.getContext('2d');

  let currentX = ev.pageX - canvasPosition.x;
  let currentY = ev.pageY - canvasPosition.y;

  ctx.lineJoin = 'round';
  ctx.strokeStyle = this.selectedColor;
  ctx.lineWidth = this.lineWidth;

  ctx.beginPath();
  ctx.moveTo(this.saveX, this.saveY);
  ctx.lineTo(currentX, currentY);
  ctx.closePath();

  ctx.stroke();

  this.saveX = currentX;
  this.saveY = currentY;
}

exportCanvasImage() {
  var dataUrl = this.canvasElement.toDataURL();

  // Clear the current canvas
  let ctx = this.canvasElement.getContext('2d');
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);


  if (this.plt.is('cordova')) {
    const options: Base64ToGalleryOptions = { prefix: 'canvas_', mediaScanner:  true };

    this.base64ToGallery.base64ToGallery(dataUrl, options).then(
      async res => {
        const toast = await this.toastCtrl.create({
          message: 'Image saved to camera roll.',
          duration: 2000
        });
        toast.present();
      },
      err => console.log('Error saving image to gallery ', err)
    );
  } else {
    // Fallback for Desktop
    var data = dataUrl.split(',')[1];
    let blob = this.b64toBlob(data, 'image/png');

    var a = window.document.createElement('a');
    a.href = window.URL.createObjectURL(blob);
    a.download = 'canvasimage.png';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
  }
}

// https://forum.ionicframework.com/t/save-base64-encoded-image-to-specific-filepath/96180/3
b64toBlob(b64Data, contentType) {
  contentType = contentType || '';
  var sliceSize = 512;
  var byteCharacters = atob(b64Data);
  var byteArrays = [];
 
  for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
    var slice = byteCharacters.slice(offset, offset + sliceSize);
 
    var byteNumbers = new Array(slice.length);
    for (var i = 0; i < slice.length; i++) {
      byteNumbers[i] = slice.charCodeAt(i);
    }
 
    var byteArray = new Uint8Array(byteNumbers);
 
    byteArrays.push(byteArray);
  }
 
  var blob = new Blob(byteArrays, { type: contentType });
  return blob;
}

The previous snippet also contains the functions to export our canvas into a base64 string, that we can use in various ways.

In our case we implement two possibly scenarios:

  • Save the image directly to the camera roll of a user if we are inside a Cordova app (native app)
  • Create a dummy element and download the file, used inside a regular web application

If you want to store it to a local file you can also check out the previous canvas drawing tutorial or the recent explanation about the Ionic device file system.

Conclusion

Using the canvas allows you to work with images and paintings in an easy way across all platforms to implement various cases like a signature field, a simple drawing app or image manipulation for your users.

You can also find a video version of this tutorial below.

The post How to Build a Canvas Painting App with Ionic 4 appeared first on Devdactic.

How to Build Ionic 4 Apps with Chart.js

$
0
0

When you want to display charts inside your application, there’s a variety of great packages out there that you can use so the question comes up, which one should I use?

In the past we used chart.js with Firebase but the usage is slightly outdated so I thought, looking for a different package makes sense.

However, in the end I came back to ng2-charts and used it so we can create a simple financial stocks tracker that displays real data inside a graph.

ionic-4-chartjs

Why the turnaround? Let’s start by taking a look at the market options. This is my personal view and I haven’t evaluated all available packages – feel free to leave your experience with other packages below in the comments.

Oh and by the way, we are going to use the awesome free API of financialmodelingprep to get some real data!

Chart Alternatives

A quick search on Google for “angular charts” leaves us with quite a few results, so I checked the packages with the most Github stars which kinda indicates a high usage in the community.

Chartist

If you want to get the smallest, most basic package then you might already stop and drop off here. Chartist looks like the package you can use when you have simple requirements to show data inside a chart, without too many special features.

I would have been my second choice but as said in the beginning, we will use Chart.js in this tutorial. If there’s interest for a Chartist tutorial, just leave a comment below!

ngx-charts

ngx-charts was the first package I gave a try but honestly, I didn’t really enjoy the documentation and examples around it. The Swimlane team (from which we also used the datatables package) did a great job with a slightly unique approach of combiniind d3 and Angular to achieve a better performance for the charts.

However, due to missing examples the task became quite challenging and the result on a real device didn’t provide the experience I was looking for.

If you can spend some time to try out different scenarios and have complex requirements, this package might be your choice in the end.

More great chart packages

Besides these contestors I also found a library using Highcharts and another one using d3.

If you have any previous experiences with these libraries or a reason to choose them, I guess they would make for a great package as well but so far I haven’t taken a deeper look into them.

Setting up our Chart App

So finally we come to the choice of this article which is ng2-charts that’s basically an Angular wrapper around the great Chart.js library.

We can get started with a blank new Ionic app and install the wrapper and the original library:

ionic start devdacticCharts blank --type=angular
cd ./devdacticCharts
npm install ng2-charts chart.js
npm install chartjs-plugin-zoom

You can see we are also installing a zoom plugin – something I really like about Chart.js as you can extend the basic functionality with some really awesome packages.

Furthermore we need to import the httpClientModule since we want to retrieve some data from the API, but of course this is not really related to our charts. But notice that we also import the zoom plugin here. This needs to happen in order to enable it for our charts later!

Now go ahead and change your app/app.module.ts to:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HttpClientModule } from '@angular/common/http';

import 'chartjs-plugin-zoom';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, HttpClientModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

In order to use the charts on any page of your app you have to import the module into the according module of the page.

In our case this means we can import it into our home/home.module.ts like:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

import { HomePage } from './home.page';
import { ChartsModule } from 'ng2-charts';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild([
      {
        path: '',
        component: HomePage
      }
    ]),
    ChartsModule
  ],
  declarations: [HomePage]
})
export class HomePageModule {}

Now we are ready to build the real chart functionality in our Ionic app!

Building our Chart View

First of all we need some data, which we can easily grab from the well documented API. The problem you will most likely encounter is bringing your own data into the form that Chart.js expects.

In our case we iterate all the entry points for stock prices and simply push the date into one array and the close value of that day into another. Both arrays (or chartData and chartLabels specific) will be passed to the chart element inside the view, which is actually a bit different from previous versions.

Additionally we can pass options as configuration, the chart type and colors to the element in our view. Again, previously we had to construct one big object but now they are different properties passed to the right element properties inside the view.

Now go ahead with the home/home.page.ts and change it to:

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AlertController } from '@ionic/angular';
import { ChartDataSets } from 'chart.js';
import { Color, Label } from 'ng2-charts';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss']
})
export class HomePage {
  // Data
  chartData: ChartDataSets[] = [{ data: [], label: 'Stock price' }];
  chartLabels: Label[];

  // Options
  chartOptions = {
    responsive: true,
    title: {
      display: true,
      text: 'Historic Stock price'
    },
    pan: {
      enabled: true,
      mode: 'xy'
    },
    zoom: {
      enabled: true,
      mode: 'xy'
    }
  };
  chartColors: Color[] = [
    {
      borderColor: '#000000',
      backgroundColor: '#ff00ff'
    }
  ];
  chartType = 'line';
  showLegend = false;

  // For search
  stock = '';

  constructor(private http: HttpClient) {
  }

  getData() {
      this.http.get(`https://financialmodelingprep.com/api/v3/historical-price-full/${this.stock}?from=2018-03-12&to=2019-03-12`).subscribe(res => {
      const history = res['historical'];

      this.chartLabels = [];
      this.chartData[0].data = [];

      for (let entry of history) {
        this.chartLabels.push(entry.date);
        this.chartData[0].data.push(entry['close']);
      }
    });
  }

  typeChanged(e) {
    const on = e.detail.checked;
    this.chartType = on ? 'line' : 'bar';
  }
}

We can change everything dynamically, that’s why I also included a switch so we can toggle a line or a bar chart on and off!

And of course, we can simply search for a stock code like AAPTL for Apple or MSFT for Microsoft.

To do so, we have to implement an input field in our view and a button to trigger our getData() function. Besides that we simply have the chart in our view, which is based on a canvas element and the baseChart directive.

We then pass in all the data we prepared upfront like the data, labels, options and so on. You can also check out all properties here.

Now wrap up the view by changing the home/home.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Devdactic Charts
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-item>
    <ion-input type="text" [(ngModel)]="stock" placeholder="AAPL, MSFT..."></ion-input>
    <ion-button slot="end" expand="full" (click)="getData()" [disabled]="stock == ''">
      <ion-icon slot="start" name="search"></ion-icon>
      Search
    </ion-button>
  </ion-item>

  <canvas baseChart 
  [datasets]="chartData" 
  [labels]="chartLabels" 
  [options]="chartOptions" 
  [colors]="chartColors"
  [legend]="showLegend" 
  [chartType]="chartType">
  </canvas>

  <ion-card>
    <ion-card-header>
      <ion-card-title>Settings</ion-card-title>
    </ion-card-header>
    <ion-card-content>
      <ion-item>
        <ion-label>Line chart?</ion-label>
        <ion-toggle (ionChange)="typeChanged($event)" checked></ion-toggle>
      </ion-item>
      <ion-item>
        <ion-label>Show legend?</ion-label>
        <ion-toggle [(ngModel)]="showLegend"></ion-toggle>
      </ion-item>
      <ion-item>
        <ion-label>Background color</ion-label>
        <ion-input type="text" [(ngModel)]="chartColors[0].backgroundColor"></ion-input>
      </ion-item>
    </ion-card-content>
  </ion-card>
</ion-content>

At the end of our view we have a card to switch some settings to demonstrate how dynamically everything works together. Nothing fancy, just target the right properties and you are good to go!

Conclusion

Picking a chart library depends on your needs, so if you have very specific requirements it might take you some time to find the perfect fit.

If you are looking for a general robust solution for Ionic, you can’t go wrong with the approach of this tutorial!

You can also find a video version of this tutorial below.

The post How to Build Ionic 4 Apps with Chart.js appeared first on Devdactic.

Building an Authentication System with Ionic 4 and NestJS

$
0
0

Whether you celebrate Christmas or not, today is your happy day as I’ve prepared this special for all Devdactic followers!

Since I wanted to give back something to all the loyal followers of this blog, I created a little gift on one of the basics that you’ll hopefully enjoy!

Todays post is not really a tutorial, but a description of the code template that contains both a NestJS backend and also an Ionic app. As a result, once you run both parts you will have a full working authentication template ready for your next app!

ionic-nest-auth-template

You can get the whole gift package by entering your email in the box below.

Let’s talk about how and why it works, and what you need to set up upfront.

Prerequisite

It’s a good idea for both parts of the template to install Ionic and the Nest CLI locally to later build out the projects like this:

npm i -g @nestjs/cli
npm i -g ionic

Of course you could also simply install the dependencies inside the project, but having both of them globally is anyway a good idea.

Also, the backend needs a MongoDB for holding the users. Therefore, install MongoDB on your local machine and while you are at it, I recommend to get a good GUI tool for managing your database like Studio 3T.

The Nest Backend

Before you run the backend you need to set a few values, and for this you have to rename the dummy.env file to .env which is the environment used for the application.

In there you can specify the port, the URI to the MongoDB (which should work like it is, the database will automatically be created) and finally a secret for the JWT authentication.

You need to make sure to have your MongoDB up and running now, and then you can go ahead and install all dependencies and run the backend like this:

cd ./api
npm install
nest start --watch

You should see some logging about the routes being set up, and if everything works your fresh API is now running at port 5000!

The routes of the API are also included in the HolidayGift.postman_collection which you can simply import to Postman to now test your API.

The routes are:

ionic-holiday-gift-postman

The API contains everything to register users, to login, get user data and delete accounts. Basically all CRUD operations for the user domain plus a login functionality.

In terms of code, you can find all the routes inside the src/users/users.controller.ts:

import { UserDto } from './dto/user.dto';
import { UsersService } from './users.service';
import {
  Controller,
  Get,
  Res,
  HttpStatus,
  Post,
  Body,
  Put,
  NotFoundException,
  Delete,
  Param,
  UseGuards
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('users')
export class UsersController {
  constructor(private userService: UsersService) {}

  @Post()
  async addUser(@Res() res, @Body() createUserDto: UserDto) {
    try {
      const user = await this.userService.addUser(createUserDto);
      return res.status(HttpStatus.OK).json({
        msg: 'User has been created successfully',
        user
      });
    } catch (e) {
      return res.status(HttpStatus.CONFLICT).json({
        msg: 'User already exists'
      });
    }
  }

  @UseGuards(AuthGuard())
  @Get(':userID')
  async getUser(@Res() res, @Param('userID') userID) {
    const user = await this.userService.getUser(userID);
    if (!user) throw new NotFoundException('User does not exist!');
    return res.status(HttpStatus.OK).json(user);
  }

  @UseGuards(AuthGuard())
  @Put(':userID')
  async updateUser(
    @Res() res,
    @Param('userID') userID,
    @Body() createUserDto: UserDto,
  ) {
    const user = await this.userService.updateUser(userID, createUserDto);
    if (!user) throw new NotFoundException('User does not exist!');
    return res.status(HttpStatus.OK).json({
      msg: 'User has been successfully updated',
      user,
    });
  }

  @UseGuards(AuthGuard())
  @Delete(':userID')
  async deleteUser(@Res() res, @Param('userID') userID) {
    const user = await this.userService.deleteUser(userID);
    if (!user) throw new NotFoundException('User does not exist');
    return res.status(HttpStatus.OK).json({
      msg: 'User has been deleted',
      user,
    });
  }

  @UseGuards(AuthGuard())
  @Get()
  async getAllUser(@Res() res) {
    const users = await this.userService.getAllUser();
    return res.status(HttpStatus.OK).json(users);
  }
}

As you can see, all routes are also protected with a guard, and we are using JWT authentication in this API.

The logic for authentication can also be seen inside the src/authauth/users.service.ts:

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { LoginUserDto } from '../users/dto/login-user.dto';
import { UsersService } from '../users/users.service';
import { JwtPayload } from './interfaces/jwt-payload.interface';

@Injectable()
export class AuthService {

    constructor(private usersService: UsersService, private jwtService: JwtService){ }

    async validateUserByPassword(loginAttempt: LoginUserDto): Promise<any> {
        let userToAttempt: any = await this.usersService.findOneByEmail(loginAttempt.email);

        return new Promise((resolve) => {
            if (!userToAttempt) {
                resolve({success: false, msg: 'User not found'});
            }
            userToAttempt.checkPassword(loginAttempt.password, (err, isMatch) => {
                if(err) resolve({success: false, msg: 'Unexpected error. Please try again later.'});
    
                if(isMatch){
                    resolve({success: true, data: this.createJwtPayload(userToAttempt)});
                } else {
                    resolve({success: false, msg: 'Wrong password'})
                }
            });
        });
    }

    createJwtPayload(user){
        let data: JwtPayload = {
            id: user._id,
            email: user.email
        };

        let jwt = this.jwtService.sign(data);

        return {
            exp: 36000,
            token: jwt            
        }
    }

    async validateUser(payload: JwtPayload): Promise<any> {
        return await this.usersService.getUser(payload.id);
    }
}

So you can register without a JWT of course, but all other routes are protected and you need to add the Authorization filed to your header with a value of “Bearer yourJWT”.

The Ionic App

There’s not much to say about the Ionic app, simply install the dependencies like always and then run it:

cd ./app
npm install
ionic serve

Inside your src/environments/environment.ts you can configure which backend will be used, and by default it will use my Heroku deployment – you should change this soon to your own local backend!

The logic of the app includes a login and register page, and a protected inside area that only users can enter because it’s protected by the auth-guard. Additionally there is another guard that is applied to all pages that are not protected in order to automatically log in users if they were authenticated before!

The code is a pretty simple check, and because it’s a guard you won’t see a page until we really receive the authentication state from the storage. You can find it inside the src/guards/auto-login.guard.ts:

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { ApiService } from '../services/api.service';
import { take, map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class AutoLoginGuard implements CanActivate {
  
   constructor(private api: ApiService, private router: Router) { }

  canActivate(): Observable<boolean> {
    return this.api.user.pipe(
      take(1),
      map(user => {
        if (!user) {
          return true;
        } else {
          this.router.navigateByUrl('/app');
          return false;
        }
      })
    )
  }
}

Besides that, all API interaction takes place inside the src/services/api.service.ts including the management of the JWT, for which we use once again the @auth0/angular-jwt package:

import { environment } from './../../environments/environment';
import { Platform } from '@ionic/angular';
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage';
import { BehaviorSubject, Observable, from } from 'rxjs';
import { take, map, switchMap } from 'rxjs/operators';
import { JwtHelperService } from "@auth0/angular-jwt";
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
 
const helper = new JwtHelperService();
export const TOKEN_KEY = 'jwt-token';

export interface User {
  first_name: string;
  last_name: string;
  email: string;
  avatar: string;
  bio: string;
  createdAt: string;
  _id: string;
  expanded?: boolean;
}

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  public user: Observable<any>;
  private userData = new BehaviorSubject(null);
 
  constructor(private storage: Storage, private http: HttpClient, private plt: Platform, private router: Router) { 
    this.loadStoredToken();  
  }
 
  loadStoredToken() {
    let platformObs = from(this.plt.ready());
 
    this.user = platformObs.pipe(
      switchMap(() => {
        return from(this.storage.get(TOKEN_KEY));
      }),
      map(token => {
        if (token) {
          let decoded = helper.decodeToken(token); 
          this.userData.next(decoded);
          return true;
        } else {
          return null;
        }
      })
    );
  }
 
  login(credentials: {email: string, password: string }) {
    return this.http.post(`${environment.apiUrl}/auth`, credentials).pipe(
      take(1),
      map(res => {
        // Extract the JWT
        return res['token'];
      }),
      switchMap(token => {
        let decoded = helper.decodeToken(token);
        this.userData.next(decoded);
 
        let storageObs = from(this.storage.set(TOKEN_KEY, token));
        return storageObs;
      })
    );
  }
 
  register(credentials: {email: string, password: string }) {
    return this.http.post(`${environment.apiUrl}/users`, credentials).pipe(
      take(1),
      switchMap(res => {
        console.log('result: ', res);
        return this.login(credentials);
      })
    );
  }

  getUserToken() {
    return this.userData.getValue();
  }

  getUserData() {
    const id = this.getUserToken()['id'];
    return this.http.get<User>(`${environment.apiUrl}/users/${id}`).pipe(
      take(1)
    );
  }

  getAllUsers(): Observable<User[]> {
    return this.http.get<User[]>(`${environment.apiUrl}/users`).pipe(
      take(1)
    );
  }

  updateUser(id, data) {
    return this.http.put(`${environment.apiUrl}/users/${id}`, data).pipe(
      take(1)
    );
  }

  removeUser(id) {
    return this.http.delete(`${environment.apiUrl}/users/${id}`).pipe(
      take(1)
    );
  }
 
  logout() {
    this.storage.remove(TOKEN_KEY).then(() => {
      this.router.navigateByUrl('/');
      this.userData.next(null);
    });
  }
 
}

As a final word: The JWT package needs to whitelist domains for which the JWT will be injected into HTTP calls. If you follow the next step and deploy your API somewhere, you need to make sure that you add your new backend URL inside the src/app/app.module.ts:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { IonicStorageModule, Storage } from '@ionic/storage';
import { HttpClientModule } from '@angular/common/http';

import { TOKEN_KEY } from './services/api.service';
import { JwtModule, JWT_OPTIONS } from '@auth0/angular-jwt';

export function jwtOptionsFactory(storage) {
  return {
    tokenGetter: () => {
      return storage.get(TOKEN_KEY);
    },
    whitelistedDomains: ['localhost:5000', 'holidaygift.herokuapp.com'] // Add your Heroku URL in here!
  }
}
@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule,
    IonicStorageModule.forRoot(),
    HttpClientModule,
    JwtModule.forRoot({
      jwtOptionsProvider: {
        provide: JWT_OPTIONS,
        useFactory: jwtOptionsFactory,
        deps: [Storage]
      }
    })],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now let’s see how you can deploy the backend.

Deployment

For the dummy deployment I used Heroku, and you can use it as well to quickly get your API up and running not only on your local machine.

You can basically follow the official Heroku guide to create a new app and push your code to it.

In addition you need to perform 2 further steps.

Add the mLab integration

The API needs a database, and you can easily add a cloud hosted MongoDB to your Heroku app inside the resources tab like this:
ionic-nest-heroku-db

Environment Variables

The database will autoamtically write a variable like we did with our local environment file, but you also need to add the value for the JWT_SECRET which you can do inside the settings tab:
ionic-nest-config-var

Over to You

Now you can use the Heroku URL and use it in your Ionic app, and you have a full blown authentication app in your hands.

Hopefully this is the starting point for your next project, and perhaps you already had some plans for the holiday season?

Definitely let me know if you enjoy the template by tweeting at me or tag me on Instagram!

I would love to see the template in action. You can also find a short video explanation of the authentication template in the video below.

Thanks for all your support and see you again next year,
Simon

The post Building an Authentication System with Ionic 4 and NestJS appeared first on Devdactic.

How to Upload Files from Ionic to Firebase Storage

$
0
0

Firebase can be used in a lot of ways, and besides the database the second biggest feature might be its cloud storage.

In this tutorial we will continue with an app from a previous tutorial on using different media files with Ionic. You can grab the code for the previous app and apply the changes of this post, or just look how the file upload could work in general!

Finally we will be able to capture media files, store them on our device and then upload them to Firebase storage!
upload-ionic-files-firebase-storage

For this app you will also need a Firebase account and an app inside the console – both are free so go ahead and create them!

Preparing our Media App

The first change we make to our previous project is adding Firebase, and another page where we can display all files that we uploaded. Therefore run inside the folder of the previous project:

npm install firebase @angular/fire

ionic g page cloudList

npm install @ionic-native/in-app-browser
ionic cordova plugin add cordova-plugin-inappbrowser

The InAppBrowser will be used to showcase that we actually got a real URL to the files after uploading them – of course this is not really mandatory otherwise.

For our simple case we will also disable the security rules on our Storage inside Firebase, so navigate to Storage -> Rules and exchange the current rules for these, which allow read and write access to everyone:

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write;
    }
  }
}

You should of course change these after testing and have appropriate security rules in place in a real app!

Adding AngularFire and Firebase

Now it’s time to connect our Ionic app to Firebase, and for this you need the credentials of your Firebase app. You can add a new app to your Firebase project by clicking at the top left on the wheel and then Project settings. At the bottom of that page you can edit your apps or add a new web project.

Once you created this you need to copy the configuration which looks like this:

ionic-4-firebase-add-to-app

We can copy it out and paste it into our environments/environment.ts and make it look like this (but of course with your values inside):

export const environment = {
  production: false,
  firebaseConfig: {
    apiKey: '',
    authDomain: '',
    databaseURL: '',
    projectId: '',
    storageBucket: '',
    messagingSenderId: '',
    appId: ''
  }
};

With that information in place we can initialise the AngularFire module inside our app/app.module.ts and also import the AngularFireStorageModule which is needed to access the storage:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { ImagePicker } from '@ionic-native/image-picker/ngx';
import { File } from '@ionic-native/File/ngx';
import { MediaCapture } from '@ionic-native/media-capture/ngx';
import { Media } from '@ionic-native/media/ngx';
import { StreamingMedia } from '@ionic-native/streaming-media/ngx';
import { PhotoViewer } from '@ionic-native/photo-viewer/ngx';

import { AngularFireModule } from '@angular/fire';
import { environment } from '../environments/environment';
import { AngularFireStorageModule } from '@angular/fire/storage';
import { InAppBrowser } from '@ionic-native/in-app-browser/ngx';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule,
    AngularFireModule.initializeApp(environment.firebaseConfig),
    AngularFireStorageModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    ImagePicker,
    MediaCapture,
    File,
    Media,
    StreamingMedia,
    PhotoViewer,
    InAppBrowser
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now our app is connected with Firebase and ready to upload some files!

Uploading Files to Firebase Storage

In the previous tutorial we captured files and stored them on our device, so we have all information locally available. Now we want to upload a certain file from a path, and we can do this by converting it to a Blob!

The Cordova file plugin already comes with a helpful function called readAsArrayBuffer that will read a local file, but we also need to make sure that we set the right mimetype for the file.

To do so, we add a simple helper function that checks the extension of the file – of course you might have different files so feel free to extend that function to your needs.

Next we create a random id and combine it with a date to create a uniqueish name for our file inside Firebase storage.

The rest becomes super easy: We just call the upload function on the AngularFireStorage and pass the new name (or path to the file) plus the blob we created. This will return an UploadTask, which offers some cool functionality like subscribing to the changes in order to display a progress bar!

Now go ahead and change your previous app/home/home.page.ts to include the new functionality as well:

// All other imports from before!
import { AngularFireStorage } from '@angular/fire/storage';

export class HomePage implements OnInit {
    files = [];
    uploadProgress = 0;

  constructor(
    private imagePicker: ImagePicker,
    private mediaCapture: MediaCapture,
    private file: File,
    private media: Media,
    private streamingMedia: StreamingMedia,
    private photoViewer: PhotoViewer,
    private actionSheetController: ActionSheetController,
    private plt: Platform,
    private toastCtrl: ToastController,
    private storage: AngularFireStorage
  ) {}

    // All other functionality from before ...

  async uploadFile(f: FileEntry) {
    const path = f.nativeURL.substr(0, f.nativeURL.lastIndexOf('/') + 1);
    const type = this.getMimeType(f.name.split('.').pop());
    const buffer = await this.file.readAsArrayBuffer(path, f.name);
    const fileBlob = new Blob([buffer], type);

    const randomId = Math.random()
      .toString(36)
      .substring(2, 8);

    const uploadTask = this.storage.upload(
      `files/${new Date().getTime()}_${randomId}`,
      fileBlob
    );

    uploadTask.percentageChanges().subscribe(change => {
      this.uploadProgress = change;
    });

    uploadTask.then(async res => {
      const toast = await this.toastCtrl.create({
        duration: 3000,
        message: 'File upload finished!'
      });
      toast.present();
    });
  }

  getMimeType(fileExt) {
    if (fileExt == 'wav') return { type: 'audio/wav' };
    else if (fileExt == 'jpg') return { type: 'image/jpg' };
    else if (fileExt == 'mp4') return { type: 'video/mp4' };
    else if (fileExt == 'MOV') return { type: 'video/quicktime' };
  }
}

Inside the view we already had a list previously, and we just add the progress bar and another sliding button to perform the upload task, so go ahead and change the app/home/home.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic Media Capture
    </ion-title>
    <ion-buttons slot="end">
      <ion-button routerLink="/cloud-list">
        <ion-icon slot="icon-only" name="cloud-done"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-progress-bar [value]="uploadProgress" color="success"></ion-progress-bar>

  <ion-list>
    <ion-item-sliding *ngFor="let f of files">
      <ion-item (click)="openFile(f)">
        <ion-icon name="image" slot="start" *ngIf="f.name.endsWith('jpg')"></ion-icon>
        <ion-icon name="videocam" slot="start" *ngIf="f.name.endsWith('MOV') || f.name.endsWith('mp4')"></ion-icon>
        <ion-icon name="mic" slot="start" *ngIf="f.name.endsWith('wav')"></ion-icon>

        <ion-label class="ion-text-wrap">
          {{ f.name }}
          <p>{{ f.fullPath }}</p>
        </ion-label>
      </ion-item>

      <ion-item-options side="start">
        <ion-item-option (click)="deleteFile(f)" color="danger">
          <ion-icon name="trash" slot="icon-only"></ion-icon>
        </ion-item-option>
      </ion-item-options>


      <ion-item-options side="end">
        <ion-item-option (click)="uploadFile(f)" color="primary">
          <ion-icon name="cloud-upload" slot="icon-only"></ion-icon>
        </ion-item-option>
      </ion-item-options>

    </ion-item-sliding>
  </ion-list>
</ion-content>

<ion-footer>
  <ion-toolbar color="primary">
    <ion-button fill="clear" expand="full" color="light" (click)="selectMedia()">
      <ion-icon slot="start" name="document"></ion-icon>
      Select Media
    </ion-button>
  </ion-toolbar>
</ion-footer>

Now you can run your app on a device, capture a media file of any type (that will be stored on the device first) and then call the cloud upload to send it to Firebase Storage!

You can see the result by going to the Storage area of your Firebase app after you uploaded a file.

Working with Files in Firebase Storage

If you also want to list all files in your app that you’ve uploaded to Firebase, you can do it as well. At the time writing this it was not possible through AngularFire, but you can also directly use the web SDK of Firebase for cases like this.

All you have to do is call listAll on the reference to your storage folder and it will return a list of references. These references contain a lot of information, and we just extract the interesting fields and push it to our local array which we will use within our view in the next step.

The most interesting part here is perhaps the getDownloadURL, which returns a Promise. That’s why we have to wait for it (or use the async pipe in the view, whatever you prefer), but this will return the full URL to the hosted file on Firebase!

Go ahead and change your app/cloud-list/cloud-list.page.ts to:

import { Component, OnInit } from '@angular/core';
import { InAppBrowser } from '@ionic-native/in-app-browser/ngx';
import * as firebase from 'firebase/app';

@Component({
  selector: 'app-cloud-list',
  templateUrl: './cloud-list.page.html',
  styleUrls: ['./cloud-list.page.scss']
})
export class CloudListPage implements OnInit {
  cloudFiles = [];

  constructor(private iab: InAppBrowser) {}

  ngOnInit() {
    this.loadFiles();
  }

  loadFiles() {
    this.cloudFiles = [];

    const storageRef = firebase.storage().ref('files');
    storageRef.listAll().then(result => {
      result.items.forEach(async ref => {
        this.cloudFiles.push({
          name: ref.name,
          full: ref.fullPath,
          url: await ref.getDownloadURL(),
          ref: ref
        });
      });
    });
  }

  openExternal(url) {
    this.iab.create(url);
  }

  deleteFile(ref: firebase.storage.Reference) {
    ref.delete().then(() => {
      this.loadFiles();
    });
  }
}

This means we can use this URL in our app to display files directly from Storage, or like we do, even open an external browser with the URL!

Finally, you can also call different functions on the reference itself like we do in the delete function.

To wrap things up, we build a super simple list around the array inside the app/cloud-list/cloud-list.page.html like this:

<ion-header>
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-back-button defaultHref="/"></ion-back-button>
    </ion-buttons>
    <ion-title>Cloud Files</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-list>
    <ion-item *ngFor="let f of cloudFiles">
      <ion-button slot="start" (click)="openExternal(f.url)">
        <ion-icon name="open" slot="icon-only"></ion-icon>
      </ion-button>
      
      <ion-label class="ion-text-wrap">
        {{ f.name }}
        <p>{{ f.full }}</p>
      </ion-label>

      <ion-button slot="end" (click)="deleteFile(f.ref)" color="danger">
        <ion-icon name="trash" slot="icon-only"></ion-icon>
      </ion-button>
    </ion-item>
  </ion-list>
</ion-content>

Now you can upload your files, see which files were uploaded, delete them or even use their full path to the hosted file.

Conclusion

Working with Firebase storage is a great way to easily host different files that your app needs, like user avatars or other uploaded files.

The biggest challenge might actually be the conversion from file to blob, but with the functions of this tutorial you should be prepared to handle everything!

You can also find a video version of this article below.

The post How to Upload Files from Ionic to Firebase Storage appeared first on Devdactic.


How to Create a Horizontal Navigation for Ionic Desktop Views

$
0
0

When you want to use your Ionic app as a website it’s not just enough to fill the available space – some UI element should simply be different.

A good example is the horizontal navigation bar you have on most pages, that you don’t really have inside a mobile application. Today, we wanna build a dynamic view that shows a tab bar on small screens but a cool navigation bar on a bigger desktop screen!

To achieve this result, we will listen to all changes to the size of our window and react accordingly so we only show what’s currently the best UI for the user!

Preparing our App

To get started, we can use the tabs template and simply generate an additional service that we will use for all size change events, so go ahead and run:

ionic start responsiveApp tabs --type=angular
cd ./responsiveApp
ionic g service services/screensize

The general approach of this article should also work for apps with a side menu, however, with a side menu you also have the ability to use the ion-split-pane which basically acts like an open menu on bigger screens. From there it’s just a bit of CSS to make it look like a side menu on any website!

Listening for our Screen size

First of all we now want to implement our service, which holds a BehaviorSubject so all other components that are interested in this can subscribe to it from everywhere inside our application.

We will simply emit whether the current screen size means we are on a mobile (small) device or a desktop screen, you could also make this even more granular by passing objects when a certain breakpoint was hit. However, the biggest change is normally between mobile/desktop so we only care about that information.

Our service will be called from outside to set the value of the Subject, and also to return the Subject. Also, when we return the Subject as an Observable we make us of distinctUntilChanged which only emits a new value to the Observable if it’s really a new value. This means, if the result of onResize is 5 times false in a row, everyone subscribed to the Observable will only get false once, and only when the value changes to true receive the new value!

Now here’s the code for our app/services/screensize.service.ts:

import { Injectable } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class ScreensizeService {

  private isDesktop = new BehaviorSubject<boolean>(false);

  constructor() { }

  onResize(size) {    
    if (size < 568) {
      this.isDesktop.next(false);
    } else {
      this.isDesktop.next(true);
    }
  }

  isDesktopView(): Observable<boolean> {
    return this.isDesktop.asObservable().pipe(distinctUntilChanged());
  }
}

To make this a useful service we need to let it know about the change of size, and the best place to listen for those changes is at the top of our app.

Therefore, we listen to the window:resize event in our app/app.component.ts and call the function of our service with the new width of the screen:

import { Component, HostListener } from '@angular/core';

import { Platform } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { ScreensizeService } from './services/screensize.service';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss']
})
export class AppComponent {
  constructor(
    private platform: Platform,
    private splashScreen: SplashScreen,
    private statusBar: StatusBar,
    private screensizeService: ScreensizeService
  ) {
    this.initializeApp();
  }

  initializeApp() {
    this.platform.ready().then(() => {
      this.statusBar.styleDefault();
      this.splashScreen.hide();
      this.screensizeService.onResize(this.platform.width());
    });
  }

  @HostListener('window:resize', ['$event'])
  private onResize(event) {
    this.screensizeService.onResize(event.target.innerWidth);
  }
}

This will call the service quite often if you resize the window, that’s why we added the distinctUntilChanged operator.

Also, we will call the service initially when the platform is ready to set the starting value so the right UI gets displayed immediately.

Implementing the Desktop View

Now we know when the view has the size of a small device or a desktop, and we can use that information in our view. First of all we need to subscribe to the Observable and set a local value in our class which will be used in the view in the next step.

Go ahead and change the app/tabs/tabs.page.ts so we make use of our service:

import { Component } from '@angular/core';
import { ScreensizeService } from '../services/screensize.service';

@Component({
  selector: 'app-tabs',
  templateUrl: 'tabs.page.html',
  styleUrls: ['tabs.page.scss']
})
export class TabsPage {
  isDesktop: boolean;

  constructor(private screensizeService: ScreensizeService) {
    this.screensizeService.isDesktopView().subscribe(isDesktop => {
      if (this.isDesktop && !isDesktop) {
        // Reload because our routing is out of place
        window.location.reload();
      }

      this.isDesktop = isDesktop;
    });
  }
}

Nothing fancy, but one disclaimer here: Moving from mobile to desktop size works very well as you will see later, but moving back doesn’t. The reason is the internal routing of our tabs, which is messed up after we come back from a bigger screen and the routes of the tabs are somehow broken.

The fix you see in the code above is to reload the whole page if we move back from desktop to mobile. Not really seamless responsive I know.

I couldn’t find a solution for this problem, if you happen to find a solution or if you’re a member of the Ionic team please leave a comment on how to fix this!

Anyway, most users will use either screen size and not change the size of their browser all the time (like we developers do), so this is something I can personally live with because of the upsides of this approach.

Now we get to the interesting part – building a view for desktop.

The idea is to hide the whole tabs stuff when we are on desktop and use a completely different markup while still maintaining routing and pages as they are.

Our desktop view consists of a header in which we will have a logo to the left and some buttons centered. The buttons have the same links to the three tabs like our tab bar, so we don’t need any other changes and all routes stay the same (hooray!).

The interesting part follows after the header and before the dummy footer: A new ion-router-outlet!

This basically means whatever the Angular router things is the right information for the URL will be displayed in there. We also use this in the side menu, and if you create other layouts with pure Angular you might have used router outlets before as well.

Now go ahead and change the app/tabs/tabs.page.html to:

<ion-tabs *ngIf="!isDesktop">
  <ion-tab-bar slot="bottom">
    <ion-tab-button tab="tab1">
      <ion-icon name="flash"></ion-icon>
      <ion-label>Tab One</ion-label>
    </ion-tab-button>

    <ion-tab-button tab="tab2">
      <ion-icon name="apps"></ion-icon>
      <ion-label>Tab Two</ion-label>
    </ion-tab-button>

    <ion-tab-button tab="tab3">
      <ion-icon name="send"></ion-icon>
      <ion-label>Tab Three</ion-label>
    </ion-tab-button>
  </ion-tab-bar>
</ion-tabs>

<div *ngIf="isDesktop" class="desktop">
  <ion-header mode="ios">
    <ion-toolbar>
      <ion-row class="ion-align-items-center">
        <ion-col size="2">
          <img src="./assets/logo.png" class="logo ion-text-left">
        </ion-col>
        <ion-col size="10">
          <div class="navbar" class="ion-text-center">
            <ion-button fill="clear" routerLink="/tabs/tab1" routerDirection="root" routerLinkActive="active-link"
              class="link">
              Tab 1
            </ion-button>
            <ion-button fill="clear" routerLink="/tabs/tab2" routerDirection="root" routerLinkActive="active-link"
              class="link">
              Tab 2
            </ion-button>
            <ion-button fill="clear" routerLink="/tabs/tab3" routerDirection="root" routerLinkActive="active-link"
              class="link">
              Tab 3
            </ion-button>
          </div>
        </ion-col>
      </ion-row>
    </ion-toolbar>
  </ion-header>

  <ion-router-outlet class="desktop-wrapper"></ion-router-outlet>

  <div class="footer">
    <span>Ionic Academy 2019</span>
  </div>

</div>

All of this means: On a bigger screen, our different markup will be shown with our custom header bar, and we added the router outlet to define where in that layout the actual information of a page should be displayed!

This works by now already pretty fine, but with some additional CSS we can make it even more awesome.

Applying custom Desktop Styling

To make the navigation bar really stand out and make use of the active link class, we need to add a few CSS rules in our app/tabs/tabs.page.scss:

.desktop {
  ion-router-outlet {
    margin-top: 56px;
    margin-bottom: 56px;
  }

  .logo {
    max-height: 40px;
  }

  ion-toolbar {
    --background: #374168;
  }

  .link {
    --color: var(--ion-color-light);
  }

  .active-link {
    --color: var(--ion-color-primary);
  }
}

.footer {
  width: 100%;
  color: #fff;
  font-weight: bold;
  background: #374168;
  height: 56px;
  line-height: 56px;
  text-align: center;
  position: fixed;
  bottom: 0px;
}

This also defines our footer and adds margin for the content that is displayed in the ion-router-outlet, since otherwise the content would be covered by our additional custom navigation bar and footer.

If you also want to globally add some styling to all pages, you can do it with the desktop-wrapper rule we added to the outlet. This means, everything we put in here affects the pages that are displayed in our layout.

You could for example make the title of each page aligned in the center and remove the shadow of the element like this in your src/global.scss (after all the imports, don’t delete them!):

.desktop-wrapper {
  ion-toolbar {
    text-align: center;
  }
  
  ion-header {
    &.header-md:after {
      background: none;
    }
  }
}

Conclusion

Desktop websites with Ionic are a challenging topic, and hopefully we see more on this in the future from the Ionic team. There are already great responsive components like the grid or CSS utilities based on breakpoints, but sometimes a completely different UI pattern is needed for a website!

You can also find a video version of this tutorial below.

The post How to Create a Horizontal Navigation for Ionic Desktop Views appeared first on Devdactic.

How to Build a PWA QR Code Scanner with Ionic for iOS & Android

$
0
0

If you want to build a QR scanner into your Ionic app, you are using a Cordova plugin most of the time. But what if you want to use it as a website, or perhaps PWA?

Just recently a member of the Ionic Academy asked this question in our community, and here’s finally a way to build a QR scanner with Ionic – by simply relying on the web API and one Javascript package. No Corodova, no Capacitor!
ionic-qr-scanner-pwa

In this tutorial we will use the jsQR package which will read the image data of a stream (using an additional canvas) to grab any QR code that might be inside the image/frame.

If you want to test it upfront, you can also find my demo application hosted on Firebase: https://devdacticimages.firebaseapp.com/home

And of course you can install it as a PWA (I just didn’t change the default icons/names. Deal with that!).

Getting Started

To get started with our QR scanner, simply start a blank new project and install the before mentioned package:

ionic start qrScanner blank --type=angular
cd ./qrScanner
npm install jsqr

The good thing about jsQR is that it is written in Typescript so we don’t need any additional steps to make it work in our app.

Creating the QR Scanner View

Before we dive into the JS logic for the scanner, let’s create a simple view. We need a bunch of buttons, some of them hidden in certain states (which we will explore soon) and 3 elements that deserve special attention:

  • Right at the top a file input – this is actually a fallback for when you don’t want a camera stream and just snap a picture or load a photo. The input is hidden and will be triggered by the a button, which we will do at the end of the tutorial
  • The video element. Inside this element we will render the preview of the selected camera once we start scanning
  • A Canvas element. This element will be hidden all the time and is used to render a frame of the video, grab the image data and pass it to the jsQR lib so it can recognise a code in the image

The rest of the view isn’t really interesting, so for now go ahead by changing your app/home/home.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic QR Scanner
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <!-- Fallback for iOS PWA -->
  <input #fileinput type="file" accept="image/*;capture=camera" hidden (change)="handleFile($event.target.files)">

  <!-- Trigger the file input -->
  <ion-button expand="full" (click)="captureImage()">
    <ion-icon slot="start" name="camera"></ion-icon>
    Capture Image
  </ion-button>

  <ion-button expand="full" (click)="startScan()">
    <ion-icon slot="start" name="qr-scanner"></ion-icon>
    Start scan
  </ion-button>

  <ion-button expand="full" (click)="reset()" color="warning" *ngIf="scanResult">
    <ion-icon slot="start" name="refresh"></ion-icon>
    Reset
  </ion-button>

  <!-- Shows our camera stream -->
  <video #video [hidden]="!scanActive" width="100%"></video>

  <!-- Used to render the camera stream images -->
  <canvas #canvas hidden></canvas>

  <!-- Stop our scanner preview if active -->
  <ion-button expand="full" (click)="stopScan()" color="danger" *ngIf="scanActive">
    <ion-icon slot="start" name="close"></ion-icon>
    Stop scan
  </ion-button>

  <ion-card *ngIf="scanResult">
    <ion-card-header>
      <ion-card-title>QR Code</ion-card-title>
    </ion-card-header>
    <ion-card-content>
      {{ scanResult }}
    </ion-card-content>
  </ion-card>

</ion-content>

The different variables like scanActive or scanResult will make more sense soon!

Connecting our View

Now we start with the initial setup of our class. We need access to a few view elements, so we make them available through the @ViewChild annotation. Because we also need access to the native elements, we store a reference to them inside the ngAfterViewInit to make life easier!

In the constructor I also added a little check because…

Disclaimer: The live QR scanner won’t work inside a PWA installed on iOS. The scanner works in the regular Safari browser, but not when installed as a PWA that runs in “standalone” mode. It’s a security topic, and we don’t know when Apple will change this behaviour and allow access to the device media from the PWA context.

Therefore you could hide the scan elements if you detect iOS and the standalone mode, in all other cases this app works fine. The fallback with the file input is the attempt to at least offer the general functionality inside an iOS PWA!

Besides that the base for our class has some helpers, but nothing fancy so far. Go ahead and change your app/home/home.page.ts to:

import { Component, ViewChild, ElementRef } from '@angular/core';
import { ToastController, LoadingController, Platform } from '@ionic/angular';
import jsQR from 'jsqr';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss']
})
export class HomePage {
  @ViewChild('video', { static: false }) video: ElementRef;
  @ViewChild('canvas', { static: false }) canvas: ElementRef;
  @ViewChild('fileinput', { static: false }) fileinput: ElementRef;

  canvasElement: any;
  videoElement: any;
  canvasContext: any;
  scanActive = false;
  scanResult = null;
  loading: HTMLIonLoadingElement = null;

  constructor(
    private toastCtrl: ToastController,
    private loadingCtrl: LoadingController,
    private plt: Platform
  ) {
    const isInStandaloneMode = () =>
      'standalone' in window.navigator && window.navigator['standalone'];
    if (this.plt.is('ios') && isInStandaloneMode()) {
      console.log('I am a an iOS PWA!');
      // E.g. hide the scan functionality!
    }
  }

  ngAfterViewInit() {
    this.canvasElement = this.canvas.nativeElement;
    this.canvasContext = this.canvasElement.getContext('2d');
    this.videoElement = this.video.nativeElement;
  }

  // Helper functions
  async showQrToast() {
    const toast = await this.toastCtrl.create({
      message: `Open ${this.scanResult}?`,
      position: 'top',
      buttons: [
        {
          text: 'Open',
          handler: () => {
            window.open(this.scanResult, '_system', 'location=yes');
          }
        }
      ]
    });
    toast.present();
  }

  reset() {
    this.scanResult = null;
  }

  stopScan() {
    this.scanActive = false;
  }
}

Using the Camera Stream to Scan a QR Code

It’s about time to scan! To start the scanner, we need to set up a few things before we start the scanning process:

  1. Get a stream of video by passing your desired constraints to getUserMedia
  2. Set the stream as the source of our video object
  3. Start our scan function with every redraw by passing it to requestAnimationFrame

We also need to bind(this) to the call of our scan function to preserve the context of “this” and allow access of our class variables and functions.

Inside the scan function we have to wait until the stream delivers enough data and then perform our magic.

With every redraw of the view, we paint the current frame of the video element on the canvas, then call getImageData() to get the image from the canvas and finally pass this image information to the jsQR library in order to check for a QR code!

That’s what happening inside our function, and if we don’t find a code we simply continue with the process.

Only if we encounter a real code (or the scanner was stopped through setting the scanActive to false) we stop calling the function again and instead present a toast with the information. The assumption here was a website inside the QR code, so therefore we can immediately open it.

With all of that theory in place, paste the below code into your existing app/home/home.page.ts HomePage class:

async startScan() {
  // Not working on iOS standalone mode!
  const stream = await navigator.mediaDevices.getUserMedia({
    video: { facingMode: 'environment' }
  });

  this.videoElement.srcObject = stream;
  // Required for Safari
  this.videoElement.setAttribute('playsinline', true);

  this.loading = await this.loadingCtrl.create({});
  await this.loading.present();

  this.videoElement.play();
  requestAnimationFrame(this.scan.bind(this));
}

async scan() {
  if (this.videoElement.readyState === this.videoElement.HAVE_ENOUGH_DATA) {
    if (this.loading) {
      await this.loading.dismiss();
      this.loading = null;
      this.scanActive = true;
    }

    this.canvasElement.height = this.videoElement.videoHeight;
    this.canvasElement.width = this.videoElement.videoWidth;

    this.canvasContext.drawImage(
      this.videoElement,
      0,
      0,
      this.canvasElement.width,
      this.canvasElement.height
    );
    const imageData = this.canvasContext.getImageData(
      0,
      0,
      this.canvasElement.width,
      this.canvasElement.height
    );
    const code = jsQR(imageData.data, imageData.width, imageData.height, {
      inversionAttempts: 'dontInvert'
    });

    if (code) {
      this.scanActive = false;
      this.scanResult = code.data;
      this.showQrToast();
    } else {
      if (this.scanActive) {
        requestAnimationFrame(this.scan.bind(this));
      }
    }
  } else {
    requestAnimationFrame(this.scan.bind(this));
  }
}

Now you are able to start the scanner from a browser (no Cordova!) and also inside a PWA. Let’s just quickly add our special iOS fallback..

Getting a QR Code from a single image

If you want a fallback for iOS or just in general a function to handle a previously capture photo, we can follow the same pattern like before.

This time we simply use the result of the file input (which we trigger with a click from code) and create a new Image from the file.

The image will be drawn on the canvas, we grab the image data and pass it to jsQR – same process like before!

So if you want this additional, simply put the code below after your previous functions:

captureImage() {
  this.fileinput.nativeElement.click();
}

handleFile(files: FileList) {
  const file = files.item(0);

  var img = new Image();
  img.onload = () => {
    this.canvasContext.drawImage(img, 0, 0, this.canvasElement.width, this.canvasElement.height);
    const imageData = this.canvasContext.getImageData(
      0,
      0,
      this.canvasElement.width,
      this.canvasElement.height
    );
    const code = jsQR(imageData.data, imageData.width, imageData.height, {
      inversionAttempts: 'dontInvert'
    });

    if (code) {
      this.scanResult = code.data;
      this.showQrToast();
    }
  };
  img.src = URL.createObjectURL(file);
}

Now you got everything covered, from live stream to photo handling with QR codes.

Hosting Your PWA

If you want to check it out as your own little PWA, you can simply follow the official Ionic PWA guide.

The steps are basically these:

ng add @angular/pwa
firebase init --project YOURPROJECTNAME

ionic build --prod
firebase deploy

Add the schematics, which makes your app PWA ready. Then connect it to Firebase (if you want, or host on your own server), run the Ionic build process and deploy it!

Conclusion

It’s always amazing to see what the web and browser are already capable of. If we didn’t have the small issue with iOS, this would be the perfect cross-platform solution for a Javascript QR Code scanner. Hopefully we will see changes in the future around this topic from Apple!

If you want to see more about PWA functionality with Ionic, just leave a comment below. I’d love to do more with PWAs since it’s a super hot and interesting topic.

You can also find a video version of this tutorial below.

The post How to Build a PWA QR Code Scanner with Ionic for iOS & Android appeared first on Devdactic.

How to Setup Universal Links in Ionic (iOS & Android)

$
0
0

It is possible to open your iOS and Android app directly through a special scheme or even a standard link these days – but the setup isn’t super easy.

In this tutorial we will go through every step to configure universal links for iOS and app links on Android. They are basically the same but have a different name. For simplicity, let’s just refer to it as deeplinks.

ionic-universal-links-ios

We will then be able to dive directly into a certain page of our Ionic app by simply opening a link inside an email, or like in the gif below with a special bar inside Safari!

App Setup

Let’s start with the Ionic part. We are going to build an app that allows to open a certain page and show a post from this blog. This means, we will be able to open the app with e.g. “https://devdactic.com/horizontal-navigation-ionic-desktop/” and within our app, we will open a page and have access to the slug of our WordPress post which is “horizontal-navigation-ionic-desktop”.

With this information we can use the WP API to grab the whole article and display it.

But there are tons of use cases, just look at the Amazon app: If you got the app, all links to Amazon products will automatically open the app on your device!

Go ahead and create the app:

ionic start devdacticLinks blank --type=angular
cd devdacticWordpress
 
ionic g page pages/posts
ionic g page pages/post

npm install @ionic-native/deeplinks

cordova plugin add ionic-plugin-deeplinks --variable URL_SCHEME=devdactic --variable DEEPLINK_SCHEME=https --variable DEEPLINK_HOST=devdactic.com

We are also using the deeplinks plugin, which will set some information for our native platforms. We pass 3 values to it, which you should change to your values:

  • URL_SCHEME: A custom URL scheme, which was used in the past to open apps like devdactic://app/whatever
  • DEEPLINK_SCHEME: Keep this to https, it’s needed on Android anyway
  • DEEPLINK_HOST: The host you want to use for your URLs. You need to have access to the domain and hosting to upload files later!

Now go ahead and import our plugin and add the HttpClientModule to our app/app.module.ts:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HttpClientModule } from '@angular/common/http';
import { Deeplinks } from '@ionic-native/deeplinks/ngx';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, HttpClientModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    Deeplinks
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

As planned in the beginning, we want to have a page where we can pass information to. We won’t really use our other post list page, but you could follow the original WordPress tutorial to build that list as well!

So open the app/app-routing.module.ts and change our routing to:

import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  { path: '', redirectTo: 'posts', pathMatch: 'full' },
  {
    path: 'posts',
    loadChildren: () => import('./pages/list/list.module').then( m => m.ListPageModule)
  },
  {
    path: 'posts/:slug',
    loadChildren: () => import('./pages/post/post.module').then( m => m.PostPageModule)
  },
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }

We are now able to route to the posts/:slug page, which we will implement soon.

For now, let’s configure our deeplinks. We can use the installed plugin to match incoming routes and then perform certain actions.

Normally this call used the page and navigation controller directly to open a page, but since v4 I couldn’t find a way to make this work in the expected way. You can still see the initial behaviour in the docs.

Instead, we want to catch the incoming route (the first parameter) and pass “posts” as the second value. The reason is that we are then able to construct the path inside our app based on these values and arguments which we can access inside the subscribe block.

Finally we are able to route inside our app with these values to open our planned page, so go ahead and change the app/app.component.ts:

import { Component } from '@angular/core';

import { Platform } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { Deeplinks } from '@ionic-native/deeplinks/ngx';
import { Router } from '@angular/router';
import { NgZone } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss']
})
export class AppComponent {
  constructor(
    private platform: Platform,
    private splashScreen: SplashScreen,
    private statusBar: StatusBar,
    private deeplinks: Deeplinks,
    private router: Router,
    private zone: NgZone
  ) {
    this.initializeApp();
  }

  initializeApp() {
    this.platform.ready().then(() => {
      this.statusBar.styleDefault();
      this.splashScreen.hide();
      this.setupDeeplinks();
    });
  }

  setupDeeplinks() {
    this.deeplinks.route({ '/:slug': 'posts' }).subscribe(
      match => {
        console.log('Successfully matched route', match);

        // Create our internal Router path by hand
        const internalPath = `/${match.$route}/${match.$args['slug']}`;

        // Run the navigation in the Angular zone
        this.zone.run(() => {
          this.router.navigateByUrl(internalPath);
        });
      },
      nomatch => {
        // nomatch.$link - the full link data
        console.error("Got a deeplink that didn't match", nomatch);
      }
    );
  }
}

Through this transformation we basically enter the app with a full URL like https://devdactic.com/ionic-4-wordpress-client/ which now becomes /posts/ionic-4-wordpress-client inside our app!

Let’s finally implement our details page, which is of course just an example – but an example on how you can pass the values from the initial real world URL to the page inside your app!

So we are using some code from our initial WordPress tutorial to grab the post data based on the slug that we can now access from the URL.

Change the app/pages/post/post.page.ts to this:

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs/operators';

@Component({
  selector: 'app-post',
  templateUrl: './post.page.html',
  styleUrls: ['./post.page.scss']
})
export class PostPage implements OnInit {
  post = null;

  constructor(private http: HttpClient, private route: ActivatedRoute) {}

  ngOnInit() {
    let slug = this.route.snapshot.paramMap.get('slug');
    let url = `https://devdactic.com/wp-json/wp/v2/posts?slug=${slug}&_embed`;

    this.http
      .get<any[]>(url)
      .pipe(
        map(res => {
          let post = res[0];
          // Quick change to extract the featured image
          post['media_url'] =
            post['_embedded']['wp:featuredmedia'][0]['media_details'].sizes['medium'].source_url;
          return post;
        })
      )
      .subscribe(post => {
        this.post = post;
      });
  }
}

Now with the post data in place, a super simple view for this page inside app/pages/post/post.page.html could look like this:

<ion-header>
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-back-button defaultHref="/posts"></ion-back-button>
    </ion-buttons>
    <ion-title>{{ post?.title.rendered }}</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content class="ion-padding">

  <div *ngIf="post">
    <img [src]="post.media_url" [style.width]="'100%'">
    <div [innerHTML]="post.content.rendered" padding></div>
  </div>

</ion-content>

That’s all we need for the Ionic app. We can actually run this app already in the browser, which will display an empty list. But you can manually navigate to the details page by including the slug in the URL to show the data from WordPress and to see that everything works!

It’s actually important to test drive it before we go any further because from now on, things can get tricky and ugly if you just make a tiny mistake…

WordPress Fix

First fix if you don’t get any WordPress data (if you are using your own WP instance) is to allow the Ionic origin which is used when performing a call against the WP API from a device.

This has been the issue for many devs here, where the Ionic app with WordPress worked fine inside the browser but not on a device.

To fix this, you can add the following snippet to the functions.php file of your theme:

add_filter('kses_allowed_protocols', function($protocols) {
    $protocols[] = 'ionic';
    return $protocols;
});

Yes, you have to change the WP code, not your Ionic app!

Android Setup

Now we want to make our links work on Android, where the name for these special links is app links.

First of all a tiny fix that you can add inside the config.xml to make the Android app launch only once from URL:

<preference name="AndroidLaunchMode" value="singleTask" />

Now we need to take a few steps to verify that we own a URL and that we have a related app:

  1. Generate a keystore file used to sign your apps (if you haven’t already)
  2. Get the fingerprint from the keystore file
  3. Create/generate an assetlinks.json file
  4. Upload the file to your server

So first step is to create a keystore file and get the fingerprint data. This file is used to sign your app, so perhaps you already have it. Otherwise, go ahead with these:

keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000
keytool -list -v -keystore my-release-key.keystore

Now we can use the cool tool right here to generate our file by adding your domain data and fingerprint data.

ionic-android-assetlinks

You can paste the generated information into an assetlinks.json file that you need to upload to your domain. The file content has this form:

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.your.package",
      "sha256_cert_fingerprints": [
        "YOURFINGERPRINT"
      ]
    }
  }
]

In my case, you can see the file at https://devdactic.com/.well-known/assetlinks.json and you need to upload it to the path on your domain of course as well.

Once you have uploaded the file, you can test if everything is fine right within the testing tool and the result should be a green circle:

ionic-android-assetlinks-varified

Now you just need to build your app and sign it, since I found issues when net signing my app. You can do this by running:

ionic cordova build android --release
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my-release-key.keystore platforms/android/app/build/outputs/apk/release/app-release-unsigned.apk alias_name

# You might have to use the absolute path like ~/Android/sdk/build-tools/25.0.2/zipalign
zipalign -v 4 platforms/android/app/build/outputs/apk/release/app-release-unsigned.apk devdactic.apk

adb install devdactic.apk

Run all the commands and the app will be installed on your connected device. You can now create a note and paste in a link and click it, or you can directly fake the behaviour through the shell by running:

adb shell am start -a android.intent.action.VIEW -d "https://devdactic.com/ionic-4-wordpress-client" com.devdactic.wpapp

If you are using your own domain, use that link. The “com.devdactic.wpapp” is the package name, which you have to set at the top of your config.xml.

When you performed all steps correctly, your app should open with a details page and show the information!

iOS Setup

Now we focus on iOS – so much fun!

First of all you need to be enrolled in the Apple Developer Program, which is also needed to submit your apps.

You app needs a valid identifier that you also always need when you submit your app. If you want to create a new one, just go to the identifiers list inside your account and add a new App id.

ios-app-domains

It’s important to enable Associated Domains for your app id in this screen!

In that screen you need to note 2 things (which you can see in the image above):

  • The bundle id (app id) you specified
  • Your Team id

Now we need to create another validation file, which is called apple-app-site-association. Without any ending, only this name!

The content should look like this, but of course insert your team id and bundle ID, for example “12345.com.devdactic.wpapp”:

{
    "applinks": {
        "apps": [],
        "details": [
            {
                "appID": "YOURTEAMID.com.your.bundleid",
                "paths": ["*"]
            }
        ]
    }
}

Next step, upload the validation file to your hosting. You can add the file to the same .well-known folder we used for Android, and your file needs to be accessible on your domain.

You can find my file here: http://devdactic.com/.well-known/apple-app-site-association

The file validates your domain for iOS, and you can also specify which paths should match. I used the * wildcard to match any routes, but if you only want to open certain paths directly in the app you could specify something like “products/*” or event multiple different paths!

If you think you did everything correctly, you can insert your data in this nice testing tool for iOS.

Perhaps your result is the same like I got, because it shows a critical error that we need to fix:

ionic-ios-file-error

We need to set the right content type for the response of the validation file!

This now depends on your hosting, here’s the solution I could use. I edited my /etc/apache2/sites-available/default-ssl.conf on my server and added this short snippet:

<Directory /var/www/.well-known/>
<Files apple-app-site-association>
Header set Content-type "application/pkcs7-mime"
</Files>
</Directory>

Now the testing tool should mark the header as set correctly, the rest below isn’t important anymore. You don’t need to sign the file anymore, that was only needed in the past!

The last step is to add the domains to your Xcode plist. You can do this directly inside Xcode by adding a new entry and using the format “applinks:yourdomain.com“.

But a better idea is actually to automate the whole process, and you can do this by adding the following snippet into the iOS section of your config.xml:

<config-file parent="com.apple.developer.associated-domains" target="*-Debug.plist">
            <array>
                <string>applinks:devdactic.com</string>
            </array>
        </config-file>
        <config-file parent="com.apple.developer.associated-domains" target="*-Release.plist">
            <array>
                <string>applinks:devdactic.com</string>
            </array>
        </config-file>

Of course you need to change the value to use your domain, but then it will be automatically added and you don’t need to change the Xcode settings manually!

Conclusion

If you followed all steps correctly you should be able to open your app through a regular link to your domain!

In case it doesn’t work….

  • Use the validation tools and check if your files are accessible correctly
  • Completely remove the app and install it again while testing
  • If your app opens on the list page: Add some logs to the deeplink match function (inside subscribe) to see if your app is actually getting there, and see which arguments you got there

It’s a tricky process, but once you’ve set up everything one time you have a great addition to your Ionic app!

You can also find a video version of this tutorial below.

The post How to Setup Universal Links in Ionic (iOS & Android) appeared first on Devdactic.

How to Create an Email List with Firebase, Sendfox and Ionic

$
0
0

If you want to integrate an email list for your app or website, you have countless options – but today I’ll show you a really simple way to make everything work with Firebase cloud functions!

Integrating an email provider can be challenging, especially if they only offer pre-made widgets. With Sendfox, we have access to an API, but we shouldn’t use it directly from our frontend app since that would expose our private keys.

ionic-firebase-sendfox-overview

Instead, we use Firebase cloud functions and database trigger to automatically add new subscribers in a safe way!

Email List Setup

First of all you need a Sendfox account, which is free in the beginning.

After creating your account, you need to create a list to which we can later add new subscribers.

You can then find the list ID in the URL of your browser, which we will need at a later point as well.

Finally we also need an API key, so go ahead and create one in your settings and save the value for now somewhere.
sendfox-api-key

The key is shown only once, so note it now before dismissing the view.

Firebase Settings

Next step is Firebase, and you need to create a new project (or use any existing). Inside Firebase, make sure you have enabled the Firestore database by clicking “Get Started” in the right tab.

In order to connect our Ionic app to Firebase we also need the configuration object. You can create it by going to
settings -> general -> add app inside Firebase, which will show an object the we need in the next step.

Ionic App Setup

Now let’s move to our app. We can create a blank new Ionic app and install the packages for Angular Fire in order to easily access both the database and the cloud functions.

Go ahead by running:

ionic start devdacticFirelist blank --type=angular
cd ./devdacticFirelist
npm i firebase @angular/fire

Now it’s time to copy over your configuration object from Firebase, which you can paste into the environments/environment.ts under a new Firebase key like this:

export const environment = {
  production: false,
  firebase: {
    apiKey: "",
    authDomain: "",
    databaseURL: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: "",
    appId: ""
  }
};

To connect everything, we need to initialize AngularFire with this object. Also, we need to import all the modules that we need within our app later, so change the app.module.ts to:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { AngularFireModule } from '@angular/fire';
import { environment } from '../environments/environment';
import { AngularFirestoreModule } from '@angular/fire/firestore';
import { AngularFireFunctionsModule, FUNCTIONS_REGION } from '@angular/fire/functions';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [
    BrowserModule,
    IonicModule.forRoot(),
    AppRoutingModule,
    AngularFireModule.initializeApp(environment.firebase),
    AngularFirestoreModule,
    AngularFireFunctionsModule
  ],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    { provide: FUNCTIONS_REGION, useValue: 'us-central1' }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

We also need to set a specific region for our functions – you will see in which region your functions run later when you deploy them. If you see a different value then, make sure to change it in here as well!

Ionic Firebase Integration

In terms of our app we will keep it simple for this example, especially the part of the view. We only need the mandatory input fields and buttons to trigger our actions, so go ahead and change the home/home.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic Firelist
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <div class="ion-padding">
    <ion-item>
      <ion-label position="floating">Email</ion-label>
      <ion-input [(ngModel)]="userdata.email"></ion-input>
    </ion-item>
    <ion-item>
      <ion-label position="floating">First Name</ion-label>
      <ion-input [(ngModel)]="userdata.first_name"></ion-input>
    </ion-item>

    <ion-button expand="full" (click)="subscribeDirectly()">
      Subscribe with Callable function</ion-button>

    <ion-button expand="full" (click)="subscribeTriggered()">
      Subscribe with DB trigger</ion-button>
  </div>
</ion-content>

As you can see from our view, we will use two different ways to add a subscriber. Not really mandatory, but a good example to show:

  • How to use a Firestore cloud trigger
  • How to use a callable cloud function

The first option is done by simply writing to the database. Our Ionic app doesn’t really know what’s happening afterwards, but we will add a cloud function later that will be called whenever we write to a specific location inside our Firestore!

The second way is directly calling a cloud function. We can call a function by its name and pass data to the call – just like we are used to with any regular Http call.

It’s important to understand that we need to call the callable() function in order to actually get an Observable to which we can subscribe, it’s not immediately created in the first line of the subscribeDirectly.

Go ahead by changing your home/home.page.ts to:

import { Component } from '@angular/core';
import { AngularFireFunctions } from '@angular/fire/functions';
import { AngularFirestore } from '@angular/fire/firestore';
import { ToastController } from '@ionic/angular';

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

  constructor(
    private functions: AngularFireFunctions,
    private db: AngularFirestore,
    private toastCtrl: ToastController
  ) {}

  subscribeTriggered() {
    this.db
      .collection('subs')
      .add({ email: this.userdata.email, first_name: this.userdata.first_name })
      .then(
        res => {
          this.showToast('You are now subscribed!');
        },
        err => {
          console.log('ERROR: ', err);
        }
      );
  }

  subscribeDirectly() {
    const callable = this.functions.httpsCallable('addSubscriber');
    const obs = callable({ email: this.userdata.email, first_name: this.userdata.first_name });
    obs.subscribe(res => {
      this.showToast(res.msg);
    });
  }

  async showToast(msg) {
    const toast = await this.toastCtrl.create({
      message: msg,
      duration: 2000
    });
    toast.present();
  }
}

In both cases we will show a toast after the operation, but in the case of the callable function we will use the message we hopefully get back from the call instead of a string directly.

Firebase Cloud Functions

Now it’s time to establish our cloud functions, since the Ionic app won’t work correctly until we have set them up.

Make sure you have the firebase-tools installed globally in order to run the other commands!

To get started, initialize a Firebase project directly at the root of your Ionic project. Inside the dialog, select only Functions (as we don’t need anything else in this example) and continue the answers like shown in the image below.

npm i -g firebase-tools
firebase init
cd ./functions
npm i axios

We also install the axios package, which is a standard helper for performing HTTP requests.

firebase-functions-init

After everything is finished, you will find a new functions folder in your project. This is the place where you develop your cloud functions for Firebase!

As mentioned before, we will implement two different ways to add a subscriber.

Our addSubscriber is a callable function, which will have a regular endpoint inside Firebase after deployment. We already called this function from Ionic directly by its name!

All data that we passed to the function is directly available, and we pass it to our addToSendfox to perform the subscription API call.

The second function newSubscriber will be triggered whenever we write data to the subs collection within Firestore (more specific: create a new document).

Inside the function we also got access to the document, which again holds all relevant information for our subscription call.

Go ahead and replace everything inside the functions/index.ts with:

import * as functions from 'firebase-functions';
const axios = require('axios').default;
const sendfoxKey = 'Bearer ' + functions.config().sendfox.key;

// Callable cloud function
export const addSubscriber = functions.https.onCall(async (data, context) => {
  addToSendfox(data);
  return { msg: 'You are now subscribed through a callable function!' };
});

// User a trigger on our database
exports.newSubscriber = functions.firestore.document('/subs/{id}').onCreate((snap, context) => {
  let user: any = snap.data();

  addToSendfox(user);
  return true;
});

function addToSendfox(user: any) {
  // Use your Sendfox List ID
  user.lists = [18645];

  const options = {
    url: 'https://api.sendfox.com/contacts',
    method: 'post',
    headers: {
      'Content-Type': 'application/json',
      Authorization: sendfoxKey
    },
    data: user
  };

  axios(options)
    .then((response: any) => {
      console.log('response: ',response);
    })
    .catch((error: any) => {
      console.log('error: ', error);
    });
}

Our helper function will perform the actual HTTP request using axios. In here we just need to construct the call to make it a regular post, with the headers we need and the body which needs to contain an array of list IDs to which we subscribed the email (remember the list ID from Sendfox you created in the beginning!).

Before we deploy our functions, we need to set they API key for Sendfox. By writing it to the Firebase environment it is stored in a safe place and not exposed to anyone else.

You can set the configuration by using functions:config:set and write whatever value you want to it.

Afterwards we are done and can continue to deploy the project, but since we only got functions we only want to deploy them:

firebase functions:config:set sendfox.key="YOURAPIKEY"
firebase deploy --only functions

After running the deployment you can also see the regions for your cloud functions. If the value here is different from what you initially entered inside your app.module.ts, go ahead and replace it with the actual value you see!

firebase-functions-deployed

Most likely your functions won’t work correctly and if you inspect the logs within the Firebase functions tab you will see a message like this:

Billing account not configured. External network is not accessible and quotas are severely limited. Configure billing account to remove these restrictions

To make an external API call from your cloud functions you need to upgrade your Firebase to a paid project. You can select it on the left hand side and pick the Blaze plan, but for some testing calls in the beginning you won’t be charged I guess (I don’t know the exact number of calls allowed).

Now the error should be gone and you can enjoy the whole process from Ionic to Firebase and Sendfox!

Conclusion

Creating an email list for your next project might look like a challenging task, but given the capabilities of email services and Firebase cloud functions make the task a simple combination of our available tools!

Of course the process of using Firebase callable functions could be used for a variety of other cases in which you want to make secured API calls to a third-party service that shouldn’t be done from your frontend project.

You can also find a video version of this tutorial below.

The post How to Create an Email List with Firebase, Sendfox and Ionic appeared first on Devdactic.

15 Angular Component Libraries you Need to Know About

$
0
0

Over the years, countless great wrappers and additional libraries for Angular appeared to help you develop your apps and websites faster without reinventing the wheel all the time.

This list is not a rating nor a full overview about the landscape of external packages for Angular. There are many more rather unnoticed small packages, but today we’ll focus on 15 well-known and proven libraries that can save you development time.

Also, creating your own Angular library isn’t actually too hard these days as well!

Let’s take a look at 15 great examples, created by the awesome Angular community around the globe. I have used most of them myself, or plan to use them in the near future.

1. Angular Material

logo-angular-material
If you want Material design for your Angular application, then Angular Material is a must have. It comes with tons of styled components and even additional features like drag&drop that you can easily add to your app.

The integration is actually super simple as this package can be integrated using Angular schematics, which will automatically update all the relevant parts of your project.

You can also check out this post on using Angular Material with Ionic.

Angular material is my first choice when building plane web applications with Angular!

2. Bootstrap

logo-bootstrap
When you are more of a Bootstrap person or have used it in the past, then the other great choice for a UI library is ng-bootstrap.

This package is basically a wrapper around Bootstrap, which usually uses jQuery for all the animations and elements. I’ve used this in the past and definitely recommend to use an Angular wrapper if possible if you want to have a Bootstrap like design in your app!

Actually there’s also another package called ngx-bootstrap, so simply pick the one you find better explained – I couldn’t really find a huge difference between them.

3. Charts

logo-chartjs
There’s a variety of packages available to present beautiful charts, and the one I tend to use is ng2-charts, which is once again an Angular wrapper around a well know package called Chart.js!

Most of the chart packages allow to create awesome charts, and this package also works very well with Ionic of course!

If you are more of a d3 person, than a great alternative would be ngx-charts as well.

4. AngularFire

If you use Firebase (and if not, why not??) you certainly want to give AngularFire a try – it’s the first thing I install whenever my project uses Firebase.

With AngularFire, you get access to a lot of helpful functionality to integrate Firebase more easily into your app. The package covers everything from Firestore, authentication, storage and even cloud functions!

You can also see Firebase storage integration for Ionic projects or Firebase with Capacitor as well.

5. JWT

logo-jwt
If you are using a backend with JWT authentication, you need to handle your token on the client side. And the package I usually install is @auth0/angular-jwt.

This library helps to easily add your JWT to all request headers and even works great with Ionic storage support.

Also, the library offers a simple service to decode token or get the expiration date, which are in itself helpful functions for any app using JWT authentication.

You can also take a look at an example login flow with Ionic!

6. Loading

When your app performs different operations through HTTP requests, you could either handle displaying of loading indicators manually all the time, or use a simple package like the ngx-loading-bar.

With this package you can define a loading in one place, and hook into any outgoing request of your app (or even the router!) to show and hide our loading bar in a non intrusive way.

If you still want to handle requests and loading indicators yourself, you could create your own Http interceptor as well.

7. Translate

Creating an app for multiple markets or languages? Then you certainly want to install ngx-translate in your app!

This package makes handling different languages super easy. This process works especially great of you plan to use it right from the beginning, as you only have to translate your localization files later and everything just works.

The package offers pipes and services to use your translation strings anywhere in your code, and you can see an example usage of the package here.

8. Pipes

logo-pipes
Looking for helpful pipes for all different cases? Then the ngx-pipes package is what you should get used to!

This package does not only contain one specific functionality, but pipes for handling strings, arrays, boolean or objects that are not included in the core Angular pipes.

You can really simplify your life with these pipes, if you manage to remember them in the right spots or simply continually use them across your projects!

9. Calendar

angular-calendar
Building a calendar is something you don’t really want to do unless you have very specific requirements that can’t be satisfied otherwise.

A great universal calendar is the angular-calendar, which comes with a lot of options and different views right out of the box.

While there are some other options for calendar components, I found this one the most flexible for Angular.

You can of course also use that calendar component with Ionic!

10. Drag & Drop

logo-dragula
For some apps, having a cool drag and drop features is a must have, and the Javascript go to package for Angular is ng2-dragula.

It’s once again a wrapper around an already existing package called Dragula, and there are countless ways to use the functionality in different scenarios plus adding constraints to the drag and drop.

As usual, there’s also no problem in using drag & drop with Dragula in Ionic!

An alternative to the package is the CDK of Angular material which also offers drag & drop, so if you already use Angular Material anyway, perhaps give it a try without bringing in another package for the same operation.

11. Data-tables

logo-datatable
Need to present a lot of data in a structured, table like representation? Then the ngx-datatable is your best friend.

This package makes displaying rows and columns super easy. There are a lot of additional features like sort, search or pagination available that you can easily enable for your own data-table with just a few lines of code.

Although a mobile client is not the perfect screen size for a table, you can still implement a data-table with Ionic as well!

12. Animations

logo-animate
From standard CSS animations to Angular animations there are countless options available to move around the elements in your app.

However, the ng-animate package is actually like a crossover with the best of 2 worlds: The package wraps the quite famous animate.css for Angular and allows to use the predefined animations directly in your Angular animation setup!

By now I haven’t used this package directly, but I used animate.css directly with Ionic and see the benefit of using this wrapper for all future projects.

13. Forms

logo-formly
Some business apps have a lot of forms to handle any kind of input, and more often than not these forms are defined by an API response.

If you don’t want to code your own logic to represent your API definition as a form, you should check out ngx-formly.

Your form will now simply have just one element, that is defined by a configuration. You might need to transform your data to make it match the configuration expected by formly, but once you have achieved that step your views will be a lot shorter and cleaner!

14. Google Maps

logo-maps
I’ve used Google maps directly in my Angular apps in the past and I’m not sure why I never used the angular-google-maps package so far?

This package is a wrapper around the standard JS SDK for Google Maps, and makes your code a lot more readable and Angularlike.

Also, you don’t have to work with a plain canvas element and instead have an Angular component with which you can work like you are used to already.

15. NgRx

logo-ngrx
The final package is something that’s on my list forever, because I never felt like getting into it. But if I ever plan a project above a certain size, a solid state management is a must have, and NgRx is the best option for that.

If you are looking for a state management solution for your app, getting into NgRx the first time might take some time, but the reward is a bullet proof system to manage the flow of data inside your app.

Conclusion

If you are looking for a specific package or wrapper for a library you intend to use, give Google a try and most likely there’ll already be something that you can make use of!

Very rarely you really have to start building a package for one of the above mentioned features, since all of them allow a lot of customisation for your own needs.

There’s also an official component search directly on the Angular page if you are looking for something else.

You can also find a video version with more explanation to the different packages below.

The post 15 Angular Component Libraries you Need to Know About appeared first on Devdactic.

How to use WordPress API Authentication with Ionic

$
0
0

If you want to use the WordPress API and connect your Ionic app to it, most likely you also want to make user of the users your page already has (or use it as a simple backend for your next app).

But instead of using the basic authentication from WordPress, a more elegant way is to use JWT authentication, which is possible with the help of a simple plugin.

ionic-wordpress-auth

In this tutorial we will prepare a WordPress instance for JWT and also build an Ionic app to register new users, and to also sign them in and make authorised requests to the WordPress API using an interceptor.

There’s also a full blown course on using Ionic with WordPress that even covers push notifications with OneSignal, so if you really want to get into this topic, check out the Ionic Academy and the course library!

WordPress Preparation

Before we get started with our app we need to prepare WordPress. First of all, we can install 2 plugins:

These plugins will help us to set up JWT authentication for the WordPress API, and also allow registeration of new users directly through the API.

Now we also need some small changes, the first one could be added to your wp-content/themes/{yourthemename}/functions.php

function add_cors_http_header(){
	header("Access-Control-Allow-Origin: *");
}
add_action('init','add_cors_http_header');

add_filter('kses_allowed_protocols', function($protocols) {
	$protocols[] = 'capacitor';
	return $protocols;
});

add_filter('kses_allowed_protocols', function($protocols) {
	$protocols[] = 'ionic';
	return $protocols;
});

This fix enables CORS and also allows different protocols – and issue a lot of you encountered with my last Ionic + WordPress tutorial on real devices!

In order to set up the JWT part, we also need to add these 2 lines to the wp-config.php file:

define('JWT_AUTH_CORS_ENABLE', true);
define('JWT_AUTH_SECRET_KEY', 'your-top-secret-key');

Generate a key or use a dummy string while testing, and then move on to the last changes.

As described in the setup guide of the JWT plugin, we also need to add the highlighted lines to our .htaccess like this:

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /wordpress/
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /wordpress/index.php [L]
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]
</IfModule>

Now the WordPress instance is ready, and hopefully everything still works with your page!

Ionic App Setup

Let’s kick off the Ionic app with a blank template, one additional service for all of our API interaction and also install Ionic storage to store our JWT later down the road:

ionic start devdacticWP blank --type=angular
cd ./devdacticWP
ionic g service services/api
npm i @ionic/storage

To make our WordPress URL easily accessible, we can set it directly inside our environments/environment.ts:

export const environment = {
  production: false,
  apiUrl: 'http://192.168.2.123:8888/wordpress/wp-json'
};

I’ve used a local MAMP testing instance and the blog was running in a folder “wordpress” – change your URL according to your settings!

Note: I recommend adding your local IP (or public website if possible), localhost won’t work once you deploy your app to a device!

Now we just need to import all relevant modules to our main module, and don’t worry the error about the interceptor, we’ll get to that file soon.

Go ahead and change your app.module.ts to:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { IonicStorageModule } from '@ionic/storage';
import { JwtInterceptor } from './interceptors/jwt.interceptor';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [
    BrowserModule,
    IonicModule.forRoot(),
    AppRoutingModule,
    HttpClientModule,
    IonicStorageModule.forRoot()
  ],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

The interceptor will be used whenever we make a HTTP request later, but for now let’s get a token first of all.

Service and Interceptor

In our service we will handle all API interaction, and also create a BehaviorSubject which represents the current state of the user. You might have used Events in the past, but since Ionic 5 you should move to RxJS instead.

Let’s go through all of our service functions quickly:

  • constructor: Load any previously stored JWT and emit the data on our Subject
  • signIn: Call the API route to sign in a user and retrieve a JWT. This token will be written to our Storage, and once this operation is finished we emit the new value on our Subject
  • signUp: Call the API route to create a new users, which is added through the plugin we installed
  • resetPassword: Call the API route to send out a reset password email, which is added through the plugin we installed
  • getPrivatePosts: Get a list of private posts from the WordPress blog – the user needs to be authenticated for this and have at least the role Editor!
  • getCurrentUser / getUserValue: Helper functions to get an Observable or the current value from our Subject
  • logout: Remove any stored token from our storage

Not a lot of logic, basically we make simply use of the WordPress API, so go ahead and add the following to your services/api.service.ts:

import { Injectable } from '@angular/core';
import { BehaviorSubject, from } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Platform } from '@ionic/angular';
import { environment } from '../../environments/environment';
import { map, switchMap, tap } from 'rxjs/operators';
import { Storage } from '@ionic/storage';

const JWT_KEY = 'myjwtstoragekey';

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  private user = new BehaviorSubject(null);

  constructor(private http: HttpClient, private storage: Storage, private plt: Platform) {
    this.plt.ready().then(() => {
      this.storage.get(JWT_KEY).then(data => {
        if (data) {
          this.user.next(data);
        }
      })
    })
  }

  signIn(username, password) {
    return this.http.post(`${environment.apiUrl}/jwt-auth/v1/token`, { username, password }).pipe(
      switchMap(data => {
        return from(this.storage.set(JWT_KEY, data));
      }),
      tap(data => {
        this.user.next(data);
      })
    );
  }

  signUp(username, email, password) {
    return this.http.post(`${environment.apiUrl}/wp/v2/users/register`, { username, email, password });
  }

  resetPassword(usernameOrEmail) {
    return this.http.post(`${environment.apiUrl}/wp/v2/users/lostpassword`, { user_login: usernameOrEmail });
  }

  getPrivatePosts() {
    return this.http.get<any[]>(`${environment.apiUrl}/wp/v2/posts?_embed&status=private`).pipe(
      map(data => {
        for (let post of data) {
          if (post['_embedded']['wp:featuredmedia']) {
            post.media_url =
              post['_embedded']['wp:featuredmedia'][0]['media_details'].sizes['medium'].source_url;
          }
        }
        return data;
      })
    );
  }

  getCurrentUser() {
    return this.user.asObservable();
  }

  getUserValue() {
    return this.user.getValue();
  }

  logout() {
    this.storage.remove(JWT_KEY).then(() => {
      this.user.next(null);
    });
  }
}

So the idea is to sign in a user, store the token, and whenever we make a following request (in our case only to load private posts), we attach the JWT to the header of our request.

And we can do this easily by implementing an interceptor, that will load the token value and if available, add it to the headers of our request.

The following code performs exactly this operation, so we don’t need any additional JWT library and can simply create a new file at app/interceptors/jwt.interceptor.ts like this:

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';
import { ApiService } from '../services/api.service';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
    constructor(private api: ApiService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        let currentUser = this.api.getUserValue();
        
        if (currentUser && currentUser.token) {
            request = request.clone({
                setHeaders: {
                    Authorization: `Bearer ${currentUser.token}`
                }
            });
        }

        return next.handle(request);
    }
}

That’s it – now we will attach the token to all requests if the user is authenticated! This means, it will not only work for the one special request that requires authentication, but all other requests to the WP API that you might make that would need authentication.

Login, Signup and Authenticated WordPress Requests

Now we basically just need to hook up our view with the functonality of our service. First of all, we need to add the ReactiveFormsModule since we will use a reactive form

Therefore go ahead and add it to the home/home.module.ts of our app:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

import { HomePage } from './home.page';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild([
      {
        path: '',
        component: HomePage
      }
    ]),
    ReactiveFormsModule
  ],
  declarations: [HomePage]
})
export class HomePageModule {}

With that in place we can create our form for the login or sign up credentials, and for simplicity we will simply create just one form for both cases.

Just note that you need to send all 3 values (username, email, password) if you want to register a new user!

Besides that we can integrate our service functionality and handle the results and errors of these calls. For the forgot password dialog we can use a simple alert, which is already the most exiting thing about our class.

We subscribe to the Observable of the user, and once we have a real user we will also trigger a reload of the private posts.

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

import { Component, OnInit } from '@angular/core';
import { ApiService } from '../services/api.service';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { AlertController, ToastController } from '@ionic/angular';

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

  user = this.api.getCurrentUser();
  posts = [];

  constructor(
    private api: ApiService,
    private fb: FormBuilder,
    private alertCtrl: AlertController,
    private toastCtrl: ToastController
  ) {
    this.user.subscribe(user => {
      if (user) {
        this.loadPrivatePosts();
      } else {
        this.posts = [];
      }
    });
  }

  ngOnInit() {
    this.userForm = this.fb.group({
      username: ['', Validators.required],
      email: '',
      password: ['', Validators.required]
    });
  }

  login() {
    this.api.signIn(this.userForm.value.username, this.userForm.value.password).subscribe(
      res => {},
      err => {
        this.showError(err);
      }
    );
  }

  signUp() {
    this.api.signUp(this.userForm.value.username, this.userForm.value.email, this.userForm.value.password).subscribe(
      async res => {
          const toast = await this.toastCtrl.create({
            message: res['message'],
            duration: 3000
          });
          toast.present();
      },
      err => {
        this.showError(err);
      }
    );
  }

  async openPwReset() {
    const alert = await this.alertCtrl.create({
      header: 'Forgot password?',
      message: 'Enter your email or username to retrieve a new password',
      inputs: [
        {
          type: 'text',
          name: 'usernameOrEmail'
        }
      ],
      buttons: [
        {
          role: 'cancel',
          text: 'Back'
        },
        {
          text: 'Reset Password',
          handler: (data) => {
            this.resetPw(data['usernameOrEmail']);
          }
        }
      ]
    });
  
    await alert.present();
  }

  resetPw(usernameOrEmail) {
    this.api.resetPassword(usernameOrEmail).subscribe(
      async res => {
        const toast = await this.toastCtrl.create({
          message: res['message'],
          duration: 2000
        });
        toast.present();
      },
      err => {
        this.showError(err);
      }
    );
  }

  loadPrivatePosts() {
    this.api.getPrivatePosts().subscribe(res => {
      this.posts = res;
    });
  }

  logout() {
    this.api.logout();
  }

  async showError(err) {
    const alert = await this.alertCtrl.create({
      header: err.error.code,
      subHeader: err.error.data.status,
      message: err.error.message,
      buttons: ['OK']
    });
    await alert.present();
  }
}

The last missing piece is our view, which basically shows our form for data plus the according buttons to trigger the actions.

We can also add a little if/else logic using ng-template to show either the card with our input fields, or a dummy card with the user value printed out as JSON.

Since the user object is an Observable we also need the async pipe, which makes the if statement looks kinda tricky (which it actually isn’t).

Below our cards we can also create a very basic list to show all private posts. Of course that’s just one idea and quick example of how to present it, you can also find a more detailed version in my previous WordPress tutorial!

Finish your app by changing the home/home.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Devdactic Wordpress
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>

  <ion-card *ngIf="!(user | async); else welcome">
    <ion-card-content>
      <form [formGroup]="userForm" (ngSubmit)="login()">
        <ion-item>
          <ion-label position="floating">Username</ion-label>
          <ion-input formControlName="username"></ion-input>
        </ion-item>
        <ion-item>
          <ion-label position="floating">Email</ion-label>
          <ion-input formControlName="email"></ion-input>
        </ion-item>
        <ion-item>
          <ion-label position="floating">Password</ion-label>
          <ion-input type="password" formControlName="password"></ion-input>
        </ion-item>

        <ion-button expand="full" [disabled]="!userForm.valid" type="submit">Sign in</ion-button>
        <ion-button expand="full" [disabled]="!userForm.valid" type="button" color="secondary" (click)="signUp()">
          Register</ion-button>
        <ion-button expand="full" type="button" color="tertiary" (click)="openPwReset()">Forgot password?</ion-button>
      </form>
    </ion-card-content>
  </ion-card>

  <ng-template #welcome>
    <ion-card>
      <ion-card-header>
        <ion-card-title>Welcome back!</ion-card-title>
      </ion-card-header>
      <ion-card-content>
        {{ ( user | async) | json }}
        <ion-button expand="full" (click)="logout()">Logout</ion-button>
      </ion-card-content>
    </ion-card>
  </ng-template>

  <ion-card *ngFor="let post of posts">
    <ion-card-header>
      <ion-card-title [innerHTML]="post.title.rendered"></ion-card-title>
      <ion-card-subtitle>{{ post.date_gmt | date }}</ion-card-subtitle>
    </ion-card-header>
    <ion-card-content>
      <img [src]="post.media_url" *ngIf="post.media_url">
      <div [innerHTML]="post.excerpt.rendered"></div>

      <!-- Example logic to open a details page below -->
      <!-- <ion-button expand="full" fill="clear" [routerLink]="['/', 'posts', post.id]" text-right>Read More...</ion-button> -->

    </ion-card-content>
  </ion-card>

</ion-content>

Now go ahead and enjoy your authentication flow from Ionic with WordPress backend!

Conclusion

Adding REST API authentication to your WordPress blog isn’t that hard and helps to create powerful apps using Ionic.

Just keep in mind that the initial role of a new user might be something that has no access to private posts yet, so you would have to manually promote users in that case.

Anyway, using WordPress as a simple backend is a great alternative to other systems if you already have a WordPress blog or know a thing or two about PHP!

You can also find a video version of this tutorial below.

The post How to use WordPress API Authentication with Ionic appeared first on Devdactic.

Building Ionic Desktop Apps with Capacitor and Electron

$
0
0

If you want to build your Ionic app for multiple platforms you can not only build it for iOS, Android and a web app – you can also use the same code for building a desktop application!

All of this can be achieved with Electron, which can simply wrap your web application inside a native container that can be used as a real native desktop application on Windows and Mac OS!

ionic-desktop-electron

We will build a simple Ionic app with Capacitor and add Electron to finally build a native desktop out of our basic application.

Setting up our App

We start with the most basic app and integrate all features one by one. So first of all we create a blank Ionic application with Capacitor support, then we need to install a few packages.

These packages will later help us to communicate with our Electron app from Angular and to package our final app.

It’s also recommended to update to the latest Angular version in case you are not yet on Angular 9.

Before you can add the Electron platform with Capacitor you also have to run an initial build, but then you can simply add the platform and open the Electron app.

Go ahead and run the following commands:

ionic start devdacticElectron blank --type=angular --capacitor
cd ./devdacticElectron

npm install ngx-electron electron
npm install electron-packager --save-dev

// If your template is not yet using Angular 9
ng update @angular/cli @angular/core --allow-dirty

// Needed to run once before adding Capacitor platforms
ionic build

npx cap add electron
npx cap open electron

Right now you might see an error – and to fix this quickly open the src/index.html and simply add a dot before the slash in this line (there is usually only a slash):

<base href="./" />

Now you can run a build again and copy all contents to your electron platform and launch the app again. These are the commands you need to run whenever you want to create an updated Electron build:

ionic build && npx cap copy
npx cap open electron

In order to prepare our app we also need to open the app/app.module.ts and add the package we installed in the beginning:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { NgxElectronModule } from 'ngx-electron';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, 
    NgxElectronModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now we are ready to build out more functionality!

Capacitor Functions & Electron IPC

Since we can use most of the Capacitor core API on all platforms, we can use the plugins within our Electron app as well!

Let’s start with the easiest part, which is a simple view to call some functions. In a real application you would have to make sure your view is responsive since the desktop window can be resized just like a browser window.

Go ahead and change the home/home.page.html to:

<ion-header>
  <ion-toolbar>
    <ion-title>
      Devdactic Electron
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-button expand="full" (click)="scheduleNotification()">Schedule Notification</ion-button>

  <ion-input [(ngModel)]="myText"></ion-input>
  <ion-button expand="full" (click)="copyText()">Copy to Clipboard</ion-button>
</ion-content>

Now the interesting part begins, and we import a few Capacitor plugins to schedule a notification or to copy some text to the clipboard, which could then be pasted anywhere outside your app!

But there’s another important part here, which happens inside our constructor: We use the ngx-electron package we installed in the beginning to communicate with the Electron main process!

There’s a main and a renderer process, and these processes can communicate with each other.

For example, you might trigger an action from the menu of your app (not like the side menu in Ionic, really the desktop menu!) and the Angular application needs to react to this.

So we can use the ipcRenderer and listen for events, in our case the event name is trigger-alert and it will simply show a modal.

Now go ahead and change your home/home.page.ts to:

import { Component } from '@angular/core';
import { Plugins } from '@capacitor/core';

const { LocalNotifications, Clipboard, Modals } = Plugins;
import { ElectronService } from 'ngx-electron';

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

  constructor(private electronService: ElectronService) {
    if (this.electronService.isElectronApp) {
      console.log('I AM ELECTRON');
      this.electronService.ipcRenderer.on('trigger-alert', this.showElectronAlert);
    }
  }

  async showElectronAlert() {
    Modals.alert({
      title: 'Hello!',
      message: 'I am from your menu :)'
    });
  }

  async scheduleNotification() {
    LocalNotifications.schedule({
      notifications: [
        {
          title: 'My Test notification',
          body: 'My notificaiton content',
          id: 1,
          schedule: { at: new Date(Date.now() + 1000 * 5) },
          sound: null,
          attachments: null,
          actionTypeId: '',
          extra: null
        }
      ]
    });
  }

  async copyText() {
    Clipboard.write({
      string: this.myText
    });

    Modals.alert({
      title: 'Ok',
      message: 'Text is in your clipboard.'
    });
  }
}

We can use the 2 functions of our app already inside Ionic serve, but the other function only works when the message is received by the renderer.

Changing your Electron App

To emit events from our Electron main process we can apply some changes inside the electron folder that Capacitor created for us.

This folder contains a index.js, which is the entry point for the Electron app. This file defines the window, the menu and can contain more code specific to Electron.

Let’s apply a little change by altering the menu inside the electron/index.js:

const menuTemplateDev = [
  {
    label: 'Options',
    submenu: [
      {
        label: 'Open Dev Tools',
        click() {
          mainWindow.openDevTools();
        },
      },
    ],
  },
  {
    label: 'Simons Tools',
    submenu: [
      {
        label: 'Trigger Menu alert',
        click() {
          mainWindow.webContents.send('trigger-alert');
        }
      },
        {
          label: 'Quit!',
          click() {
            app.quit();
          }
      }
    ]
  }
];

Replace the previously existing menu with this snippet, which adds another label and submenu with two more buttons.

To make this menu appear also in the final application, look for the line towards the end of the electron/index.js file where we set the menu of our app. Right now it’s inside an if and will only be displayed in dev mode – move the line out of the if to show it all the time!

Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplateDev));

Now we have changed the menu a bit, and we can already see the changes if we build our app again. We are now also able to send out the message from the menu, which is then recognized inside our Angular app and gives us a simple way to communicate between the two processes!

Building your Electron App

Finally you might also want to distribute your app at some point, and since we installed the Electron Packager in the beginning we can now add a small snippet to our package.json to easily build our app:

"electron:mac": "electron-packager ./electron SimonsApp --overwrite --platform=darwin --arch=x64 --prune=true --out=release-builds",
"electron:win": "electron-packager ./electron SimonsApp --overwrite --asar=true --platform=win32 --arch=ia32 --prune=true --out=release-builds --version-string.CompanyName=CE --version-string.FileDescription=CE --version-string.ProductName='Simons Electron App'"

You can now go ahead and run npm run electron:mac to build a native desktop app for Mac OS (or Windows). You will find the files inside the release-builds folder of your Ionic application, and you could use these files to distribute your application!

Conclusion

With Capacitor and the available APIs it becomes really easy to add Electron support to your Ionic app. Always make sure you get in the right mindset for desktop apps, since these will have a different size than a mobile app and need to be really responsive as well.

Also, with the inter process communication (IPC) shown in this tutorial you are ready to communicate between your Angular code and the Electron main process to build a powerful desktop application right from your Ionic Code!

You can also find a video version of this tutorial below.

The post Building Ionic Desktop Apps with Capacitor and Electron appeared first on Devdactic.


How to Apply Instagram like Photo Filters with Ionic & Capacitor

$
0
0

When you want to manipulate the images like Instagram within your Ionic app there are multiple ways to achieve a filtered photo effect, but the approach we are using today will make it unbelievable easy for you!

We will use the web photo filter package which has a great set of predefined filters that we can apply to our images, and we’ll do all of this in combination with Capacitor.

ionic-instagram-photo-filter

Because the photo filter package is also based on WebGL and is a standard web component, everything we do will work perfectly inside our browser preview – no Cordova plugins today!

Setting up the Photo Filter Project

Before we dive into the code we need a new app with Capacitor enabled (which is the recommendation as of now as well) and install the before mentioned package.

Since we are capturing images within the browser we also need to install the PWA Elements package from Capacitor which gives a nice web camera UI when capturing photos.

You can then as well add the native platforms, but you can also stick to the browser for now:

ionic start imageFilter blank --type=angular --capacitor
cd ./imageFilter
npm install web-photo-filter
npx cap init


# PWA Elements for camera preview inside the browser
npm install @ionic/pwa-elements

# Add native platforms if want
ionic build
npx cap add ios
npx cap add android

To use the package we need to perform a few steps. First, we need to add it to our app/index.html inside the head area:

<script async src="webphotofilter.js"></script>

Then we need to tell Angular to please load some assets from the package folder and move them to our final output directory. You can do this by changing the angular.json and adding the lines below inside the array of assets:

{
  "glob": "webphotofilter.js",
  "input": "node_modules/web-photo-filter/dist",
  "output": "./"
},
{
  "glob": "webphotofilter/*",
  "input": "node_modules/web-photo-filter/dist",
  "output": "./"
}

The last step is to enable the PWA elements from Capacitor, so it’s not related to the web photo filter package itself but Capacitor. For this, simply add the following lines to your app/main.ts:

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

// Call the element loader after the platform has been bootstrapped
defineCustomElements(window);

Now we are ready to create some nice filters!

Creating the Photo Filter Logic

To use the component in a page, we need to add the CUSTOM_ELEMENTS_SCHEMA to the according module of the page, which means in our case simply change your home/home.module.ts to:

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

import { HomePage } from './home.page';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild([
      {
        path: '',
        component: HomePage
      }
    ])
  ],
  declarations: [HomePage],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class HomePageModule {}

Now the fun begins, and I created an array of all the possible filters which you can also find in the web photo filter showcase.

The idea is to show a preview of all filters attached to an image inside a scroll view (slides) and a big preview of the currently selected filter (just like on IG).

So before we can attach filters, we need to capture an image using the standard Capacitor plugins. This will give back a base64 string, which might also be a problem on a device due to memory constraints.

There’s not really any filtering happening from code – we are simply setting a selected filter, which will be used by the actual component in the view in the next step.

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

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

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss']
})
export class HomePage {
  selectedFilter = '';
  selectedIndex = 0;
  result: HTMLElement;

  image: any = '';

  slideOpts = {
    slidesPerView: 3.5,
    spaceBetween: 5,
    slidesOffsetBefore: 20,
    freeMode: true
  };

  filterOptions = [
    { name: 'Normal', value: '' },
    { name: 'Sepia', value: 'sepia' },
    { name: 'Blue Monotone', value: 'blue_monotone' },
    { name: 'Violent Tomato', value: 'violent_tomato' },
    { name: 'Grey', value: 'greyscale' },
    { name: 'Brightness', value: 'brightness' },
    { name: 'Saturation', value: 'saturation' },
    { name: 'Contrast', value: 'contrast' },
    { name: 'Hue', value: 'hue' },
    { name: 'Cookie', value: 'cookie' },
    { name: 'Vintage', value: 'vintage' },
    { name: 'Koda', value: 'koda' },
    { name: 'Technicolor', value: 'technicolor' },
    { name: 'Polaroid', value: 'polaroid' },
    { name: 'Bgr', value: 'bgr' }
  ];

  constructor() {}

  async selectImage() {
    const image = await Plugins.Camera.getPhoto({
      quality: 100,
      allowEditing: false,
      resultType: CameraResultType.DataUrl,
      source: CameraSource.Camera
    });
    this.image = image.dataUrl;
  }

  filter(index) {
    this.selectedFilter = this.filterOptions[index].value;
    this.selectedIndex = index;
  }

  imageLoaded(e) {
    // Grab a reference to the canvas/image
    this.result = e.detail.result;
  }

  saveImage() {
    let base64 = '';
    if (!this.selectedFilter) {
      // Use the original image!
      base64 = this.image;
    } else {
      let canvas = this.result as HTMLCanvasElement;
      // export as dataUrl or Blob!
      base64 = canvas.toDataURL('image/jpeg', 1.0);
    }

    // Do whatever you want with the result, e.g. download on desktop
    this.downloadBase64File(base64);
  }

  // https://stackoverflow.com/questions/16996319/javascript-save-base64-string-as-file
  downloadBase64File(base64) {
    const linkSource = `${base64}`;
    const downloadLink = document.createElement('a');
    document.body.appendChild(downloadLink);

    downloadLink.href = linkSource;
    downloadLink.target = '_self';
    downloadLink.download = 'test.png';
    downloadLink.click();
  }
}

I’ve also added a function to download the image with a dummy function inside the browser (won’t work on a device like this!).

We can do this because we grab a reference to the filtered image inside the imageLoaded function, that will be called from the component once the filter is applied.

Let’s see how we can finally apply the filters based on this code.

Building the Instagram Photo Filter View

We already prepared everything we need in terms of logic, now we just need to create the UI: We can use the web photo filter in multiple places, and so we will have one bigger preview using the selectedFilter, and an iteration of slides based on the array to show all the possible filters already attached to our image.

On the component you can specify to keep the original image, which helps to also show the non filtered version – but we need some additional CSS to hide the part within the web component that we don’t need. We’ll get to that later, just note that this is where the keep property becomes relevant!

We now also see how our imageLoaded is called. Whenever the filter for the main image is applied, our function will be triggered with the details of the component.

Now go ahead and change your home/home.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic Photo Filter
    </ion-title>
    <ion-buttons slot="end" *ngIf="image">
      <ion-button (click)="saveImage()">Save</ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-button expand="full" (click)="selectImage()">Select Image</ion-button>

  <!-- Big preview of selected filter -->
  <div id="preview" *ngIf="image">
    <web-photo-filter 
      [class.no-original]="selectedFilter != ''" 
      [class.only-original]="selectedFilter == ''"
      [src]="image" 
      keep="true" 
      [filter]="selectedFilter" 
      (filterLoad)="imageLoaded($event)">
    </web-photo-filter>
  </div>

  <!-- Slides with thumbnail preview of all filters -->
  <ion-slides [options]="slideOpts" *ngIf="image">

    <ion-slide *ngFor="let opts of filterOptions; let i = index;" tappable (click)="filter(i)">
      <ion-text [class.selected]="i == selectedIndex">{{ opts.name }}</ion-text>
      <div id="preview">
        <web-photo-filter 
          [class.no-original]="i > 0" 
          [src]="image" 
          keep="false" 
          [filter]="opts.value">
        </web-photo-filter>
      </div>
    </ion-slide>

  </ion-slides>

</ion-content>

Right now this will look very odd in your view, since the element take up too much space. Therefore, let’s add some CSS to change the general UI of our page within the home/home.page.scss:

ion-slides {
  margin-top: 50px;
}

ion-slide {
  flex-direction: column;
  font-size: small;
  ion-text {
    padding: 5px;
  }
  .selected {
    font-weight: 600;
  }
}

This won’t change much by now, since we need to define the real CSS rules for the component inside the :root pseudo class, which we can also do within our app/theme/variables.scss:

:root {
  #preview {
    web-photo-filter {
      canvas, img {
        max-width: 100%;
      }
    }

    web-photo-filter.no-original {
        img {
          display: none;
        }
    }

    web-photo-filter.only-original {
      canvas {
        display: none;
      }
  }
}

// Already existing variables
--ion-color-primary...

These rules now target the component, and the reason for the different cases is simple: The web photo filter renders the original image inside an img tag, while the filtered image is displayed inside a canvas element!

So if you want to see only the original, hide the canvas. If you want to see only the filtered, hide the canvas.

We need these rules because we have set the keep property to true in our preview, otherwise we wouldn’t be able to easily switch between filtered and normal photo anymore!

Conclusion

Now you can test drive your application on the browser, capture an image and immediately apply all the filters. You can even download the selected filtered photo directly in your browser!

On a real device I had problems with this approach, perhaps because we are rendering a lot of preview images based on base64 data which might cause memory issues. In general it worked, but only if the array only contained like 3 filters.

You might have to use the real file URL after capturing an image or use a different non.Instagram like representation if you experience crashes on older devices.

You can also find a video version of this tutorial below.

The post How to Apply Instagram like Photo Filters with Ionic & Capacitor appeared first on Devdactic.

Ionic 5 Image Upload with NestJS & Capacitor: The API

$
0
0

Image upload is one of the most common use cases in your Ionic app, and to show you everything you need, we’ll build both the API and app together!

This is the first part of our mini series on image upload with Ionic. Today, we are going to implement a NestJS API that works with a MongoDB so we can upload files from our app, get a list of all uploaded images and also delete images.

ionic-nest-image-api

For today the only result we’ll have is the API which we can test with Postman, but adding some full stack skills to your toolbox is anyway a good idea!

Nest API Setup

If you haven’t worked with Nest before you need to install the CLI first. Then you can continue to generate a new project and install all dependencies that we need to integrate our MongoDB using Typegoose!

We can then continue by generating all necessary files for our Image upload using the Nest CLI again. Go ahead and run:

npm i -g @nestjs/cli
nest new imageApi
cd ./imageApi
npm i nestjs-typegoose @typegoose/typegoose mongoose @nestjs/platform-express
npm install --save-dev @types/mongoose

nest g module image
nest g controller image
nest g service image
nest g class image/image.model

In order to allow CORS requests to our Nest API we also need to add a line to the
src/main.ts now:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableCors();
  await app.listen(3000);
}
bootstrap();

To connect the Nest API to our database we also need to add the URL to the database right into the src/app.module.ts like this:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ImageModule } from './image/image.module';
import { TypegooseModule } from "nestjs-typegoose";

@Module({
  imports: [
    ImageModule,
    TypegooseModule.forRoot('mongodb://localhost:27017/imageapi', {
      useNewUrlParser: true
    })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule { }

If you already have a production DB running somewhere else you can of course use that URL as well!

Defining the Database Model

If we want to save our documents to the database we need to define the model, which also helps to have typings available. We just need to define these in one place using the annotations from Typegoose, which is the reason I added this package to the mix!

Our model will have a name and image file, so we will actually store the image as data right in the database. It’s probably not the best way, but in some cases definitely a legit alternative.

Go ahead and change the src/image/image.model.ts to:

import { prop } from '@typegoose/typegoose';
import { Schema } from 'mongoose';

export class ApiImage {
    // Created automatically, just needed for TS
    readonly _id: Schema.Types.ObjectId;

    @prop({ required: true })
    name: string;

    @prop({ default: {data: null, contentType: null} })
    image_file: {
        data: Buffer;
        contentType: string;
    };

    @prop({ default: Date.now() })
    createdAt: Date;

    // We'll manually populate this property
    url: string;
}

I left out the annotation for the url since this field shouldn’t be written to the database, but we still need it for Typescript since we want to write this manually later in this tutorial.

These objects can now be written right to the database, but before we can do this we need to tell our module about it.

Besides that we also need to add Multer, a package that is commonly used for handling file upload in Node applications. We got access to the underlying Express platform with Nest as well, it’s one of the packages we also installed in the beginning!

We will only accept image files with the filter for now, but feel free to change this to your needs.

We also pass another option to the initial call which connects typegoose with our database, since we don’t really want to have this “__v” version key in all of the database query results from MongoDB!

Now open the src/image/image.module.ts and change it to:

import { Module, HttpException, HttpStatus } from '@nestjs/common';
import { ImageController } from './image.controller';
import { ImageService } from './image.service';
import { MulterModule } from '@nestjs/platform-express';
import { extname } from 'path';
import { ApiImage } from './image.model';
import { TypegooseModule } from 'nestjs-typegoose';

const imageFilter = function (req, file, cb) {
  // accept image only
  if (!file.originalname.match(/\.(jpg|jpeg|png)$/)) {
    cb(new HttpException(`Unsupported file type ${extname(file.originalname)}`, HttpStatus.BAD_REQUEST), false);
  }
  cb(null, true);
};

@Module({
  controllers: [ImageController],
  providers: [ImageService],
  imports: [
    TypegooseModule.forFeature([{
      typegooseClass: ApiImage,
      schemaOptions: {versionKey: false}
    }]),
    MulterModule.registerAsync({
      useFactory: () => ({
        fileFilter: imageFilter
      }),
    }),
  ]
})
export class ImageModule { }

Now our module is configured and can use the database model, so we just need to create a service that interacts with the database and a controller to define the actual endpoints of the API!

Creating our API Endpoints

Before we create the endpoints, we create a service that acts as the logic between the controller and the database.

This service will read and write our documents from the MongoDB, and most of it is pretty standard CRUD functionality definition. But we also need to make sure we store our file (which will be added by Multer), so whenever we create a new document in the database we also add the buffer and mimetype from the file of the upload to our database object before we save it.

Now open the src/image/image.service.ts and change it to:

import { Injectable } from '@nestjs/common';
import { InjectModel } from 'nestjs-typegoose';
import { ReturnModelType } from '@typegoose/typegoose';
import { ApiImage } from './image.model';

@Injectable()
export class ImageService {
    constructor(
        @InjectModel(ApiImage) private readonly imageModel: ReturnModelType
    ) { }

    async create(file, createCatDto: { name: string, image_file: Buffer }) {
        const newImage = await new this.imageModel(createCatDto);
        newImage.image_file.data = file.buffer;
        newImage.image_file.contentType = file.mimetype;
        return newImage.save();
    }

    async findAll(): Promise {
        return await this.imageModel.find({}, { image_file: 0 }).lean().exec();
    }

    async getById(id): Promise {
        return await this.imageModel.findById(id).exec();
    }

    async removeImage(id): Promise {
        return this.imageModel.findByIdAndDelete(id);
    }
}

Everything is in place now, and we just need to define the endpoints and connect it with the right functions of the service we just created.

With Nest we can use a lot of annotations to mark our endpoints, and we can also use the FileInterceptor to tell Nest that we want to handle file uploads on a specific route – which is of course exactly what we want to do!

Most of the functions will simply call the according function of the service with a bit of error handling, but we also need to handle something else:

To show an actual image from our API, we need to get the data from the database and return the buffer data with the right content type back to the user. This allows to directly display an image using the according route!

The problem is that normally the database would also return the whole object for the list of images (or after upload), and this list would be a huge response including all the binary data.

To change this behaviour, we set the url property of our images now manually to the URL of our API and the right path to the image file. We also do this in the list call on all of the items that we return, and at the same time remove the image_file field since this contains the binary data that we don’t want to return to the front end!

Now it’s time to change the src/image/image.controller.ts to:

import { Controller, Post, UseInterceptors, UploadedFile, Res, Req, Body, HttpStatus, Get, Param, NotFoundException, Delete } from '@nestjs/common';
import { ImageService } from './image.service';
import { FileInterceptor } from '@nestjs/platform-express';

@Controller('image')
export class ImageController {

    constructor(private imageService: ImageService) { }

    @Post('')
    @UseInterceptors(FileInterceptor('file'))
    async uploadImage(@UploadedFile() file, @Res() res, @Req() req, @Body() body) {
        const image = await this.imageService.create(file, body);
        const newImage = image.toObject();    

        const host = req.get('host');
        newImage.image_file = undefined;
        newImage.url = `http://${host}/image/${newImage._id}`;
        
        return res.send(newImage);
    }

    @Get('')
    async getImages(@Req() req) {
        const host = req.get('host');
        const images = await this.imageService.findAll();

        images.forEach(image => {
            image.url = `http://${host}/image/${image._id}`;
        });

        return images;
    }

    @Get(':id')
    async getImage(@Res() res, @Body() body, @Param('id') id) {
        const image = await this.imageService.getById(id);
        if (!image) throw new NotFoundException('Image does not exist!');
        res.setHeader('Content-Type', image.image_file.contentType);
        return res.send(image.image_file.data.buffer);
    }


    @Delete(':id')
    async deleteImage(@Res() res, @Body() body, @Param('id') id) {
        const image = await this.imageService.removeImage(id);

        if (!image) throw new NotFoundException('Image does not exist!');
        return res.status(HttpStatus.OK).json({msg: 'Image removed.'});
    }
}

With all of that in place you can run your Nest API by using one of the commands from the package.json like:

npm run start:dev

For now we can test the API using a tool like Postman or Insomnia – go ahead and upload some files to your new API!

Conclusion

Setting up a Nest API is just as easy as creating any other Angular application – and we are now ready to upload images to our own API and list these files in a response back to the user.

We have created a decent API that can be used from any application, and in the next part we will create the Ionic app to upload images from all platforms to this API!

You can also find a video version of this tutorial below.

The post Ionic 5 Image Upload with NestJS & Capacitor: The API appeared first on Devdactic.

Ionic 5 Image Upload with NestJS & Capacitor: Capacitor App

$
0
0

This is the second part of a mini series on image upload with Ionic. In this part we’ll create an Ionic app with Capacitor so we can upload image files from the browser and iOS & Android apps!

In order to go through this tutorial make sure you have the API from the first part up and running:
Ionic 5 Image Upload with NestJS & Capacitor: The API

ionic-nest-capacitor

Today we will build the Ionic app and implement file upload with Capacitor, so we can upload images to our API from basically everywhere!

Starting our Ionic app with Capacitor

Get started by setting up a new Ionic app with Capacitor directly enabled and install the PWA elements so we can use the camera inside the browser as well.

Besides that we need just a service for our API communication and afterwards you can already build the project and add the platforms you want to target with Capacitor:

ionic start devdacticImages blank --type=angular --capacitor
cd ./devdacticImages
npm i @ionic/pwa-elements

ionic g service services/api

ionic build
npx cap add ios
npx cap add android

To make API calls we need to add the HttpClientModule as usually to our app/app.module.ts:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [
    BrowserModule,
    IonicModule.forRoot(),
    AppRoutingModule,
    HttpClientModule,
  ],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

Now we also need to load the PWA elements package we installed in the beginning by adding 2 more lines at the bottom of our src/main.ts:

import { defineCustomElements } from '@ionic/pwa-elements/loader';
defineCustomElements(window);

That’s it for the configuration today!

Creating our Image Upload Service

Let’s start with the logic inside our service, which takes care of all the API calls to our Nest backend.

We can create the same kind of interface in our app like we did for the database model in the first part, and for the most part we just need to make the appropriate call to our API in the functions.

Besides that we need a bit of special handling for the image upload, for which we got actually 2 functions:

  • uploadImage: This is the function used when dealing with blob data as a result of using the Camera or photo library
  • uploadImageFile: This is used when we pick a file with a regular file input inside a browser

For the upload we can create new FormData and append everything else we want to have in the body of our call, for now we just use a name as example but you could supply all your other mandatory information in that place.

Now go ahead and change your services/api.service.ts to:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

export interface ApiImage {
  _id: string;
  name: string;
  createdAt: Date;
  url: string;
}

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  url = 'http://localhost:3000';

  constructor(private http: HttpClient) { }

  getImages() {
    return this.http.get<ApiImage[]>(`${this.url}/image`);
  }

  uploadImage(blobData, name, ext) {
    const formData = new FormData();
    formData.append('file', blobData, `myimage.${ext}`);
    formData.append('name', name);

    return this.http.post(`${this.url}/image`, formData);
  }

  uploadImageFile(file: File) {
    const ext = file.name.split('.').pop();
    const formData = new FormData();
    formData.append('file', file, `myimage.${ext}`);
    formData.append('name', file.name);

    return this.http.post(`${this.url}/image`, formData);
  }

  deleteImage(id) {
    return this.http.delete(`${this.url}/image/${id}`);
  }
}

Not too much going on in here, let’s see how we can actually grab some images now.

Uploading Image Files

Capacitor makes it really easy to capture images across all devices, and the same code works on the web, iOS and Android! We only have one special case for image upload using a regular file input tag, which we also add as a ViewChild so we can manually trigger it and otherwise completely hide it in our DOM.

Getting our list of images and deleting an image isn’t very spectacular, but when we want to add a new image we first of all present an action sheet to select a source. We’ll create the array of available buttons upfront in order to dynamically add the file upload button if the code is running on the web on not inside a native app!

Once we capture an image (or chose from the photo library) we also need to convert this base64 string to a blob so we can send it to our API. This is just a decision we made on the API, you could also build it in a different way with only base64 data of course.

For the file input we actually don’t need any other conversion since we can directly upload this file element with a standard POSt to our API, so open your app/home/home.page.ts and change it to:

import { Component, ViewChild, ElementRef } from '@angular/core';
import { ApiService, ApiImage } from '../services/api.service';
import { Plugins, CameraResultType, CameraSource } from '@capacitor/core';
import { Platform, ActionSheetController } from '@ionic/angular';
const { Camera } = Plugins;

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  images: ApiImage[] = [];
  @ViewChild('fileInput', { static: false }) fileInput: ElementRef;

  constructor(private api: ApiService, private plt: Platform, private actionSheetCtrl: ActionSheetController) {
    this.loadImages();
  }

  loadImages() {
    this.api.getImages().subscribe(images => {
      this.images = images;
    });
  }

  async selectImageSource() {
    const buttons = [
      {
        text: 'Take Photo',
        icon: 'camera',
        handler: () => {
          this.addImage(CameraSource.Camera);
        }
      },
      {
        text: 'Choose From Photos Photo',
        icon: 'image',
        handler: () => {
          this.addImage(CameraSource.Photos);
        }
      }
    ];

    // Only allow file selection inside a browser
    if (!this.plt.is('hybrid')) {
      buttons.push({
        text: 'Choose a File',
        icon: 'attach',
        handler: () => {
          this.fileInput.nativeElement.click();
        }
      });
    }

    const actionSheet = await this.actionSheetCtrl.create({
      header: 'Select Image Source',
      buttons
    });
    await actionSheet.present();
  }

  async addImage(source: CameraSource) {
    const image = await Camera.getPhoto({
      quality: 60,
      allowEditing: true,
      resultType: CameraResultType.Base64,
      source
    });

    const blobData = this.b64toBlob(image.base64String, `image/${image.format}`);
    const imageName = 'Give me a name';

    this.api.uploadImage(blobData, imageName, image.format).subscribe((newImage: ApiImage) => {
      this.images.push(newImage);
    });
  }

  // Used for browser direct file upload
  uploadFile(event: EventTarget) {
    const eventObj: MSInputMethodContext = event as MSInputMethodContext;
    const target: HTMLInputElement = eventObj.target as HTMLInputElement;
    const file: File = target.files[0];
    this.api.uploadImageFile(file).subscribe((newImage: ApiImage) => {
      this.images.push(newImage);
    });
  }

  deleteImage(image: ApiImage, index) {
    this.api.deleteImage(image._id).subscribe(res => {
      this.images.splice(index, 1);
    });
  }

  // Helper function
  // https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript
  b64toBlob(b64Data, contentType = '', sliceSize = 512) {
    const byteCharacters = atob(b64Data);
    const byteArrays = [];

    for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
      const slice = byteCharacters.slice(offset, offset + sliceSize);

      const byteNumbers = new Array(slice.length);
      for (let i = 0; i < slice.length; i++) {
        byteNumbers[i] = slice.charCodeAt(i);
      }

      const byteArray = new Uint8Array(byteNumbers);
      byteArrays.push(byteArray);
    }

    const blob = new Blob(byteArrays, { type: contentType });
    return blob;
  }
}

Now we got a list of images from the backend, and we also got the functionality in place to upload images. We just need to create the appropriate view with a list of images, divided into columns and rows in our view.

Each column will hold the image and some information plus a button to delete that image. Remember from the first part: We created the API response exactly for this, so now the src for an image is just the URL to our API and we don’t need any additional logic to convert any data!

At the bottom we can add a nice looking fab button that floats above our content and triggers the action sheet we created, plus the previously mentioned file input that is simply hidden and only triggered through our ViewChild.

Although it’s hidden we can still capture the event when an image was selected, which will as a result upload that file using our service!

Now the last missing piece of our series goes to the app/home/home.page.html:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Devctic Images
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-row>
    <ion-col
      size="6"
      *ngFor="let img of images; let i = index;"
      class="ion-text-center"
    >
      <ion-label>
        {{ img.name }}
        <p>{{ img.createdAt | date:'short' }}</p>
      </ion-label>

      <img [src]="img.url" />
      <ion-fab vertical="bottom" horizontal="end">
        <ion-fab-button
          color="danger"
          size="small"
          (click)="deleteImage(img, i)"
        >
          <ion-icon name="trash-outline"></ion-icon>
        </ion-fab-button>
      </ion-fab>
    </ion-col>
  </ion-row>

  <ion-fab vertical="bottom" horizontal="end" slot="fixed">
    <input
      type="file"
      #fileInput
      (change)="uploadFile($event)"
      hidden="true"
      accept="image/*"
    />
    <ion-fab-button (click)="selectImageSource()">
      <ion-icon name="add"></ion-icon>
    </ion-fab-button>
  </ion-fab>
</ion-content>

Start your app, run it on the browser or deploy it to a device – the image capturing will work seamlessly everywhere!

Just a quick note: If you deploy this app to a device you need to change the URL to your backend since your device won’t be able to access the localhost URL we currently got.

To fix this, either deploy the API to some hosting or for testing use the awesome ngrok tool!

I also recommend to test your app with live reload on a device, the command for that looks like this:

npx cap run ios --livereload --external

Conclusion

We have built a powerful API with image upload in the first part and now created our Ionic app with Capacitor to interact with that API. Because of Capacitor we can also use the same interfaces across devices and the web, which makes handling files and images so much more enjoyable than in the past.

Enjoy our new project and expand it with some more functionality and let me know if you used this as the base for one of your next projects!

You can also find a video version of this tutorial below.

The post Ionic 5 Image Upload with NestJS & Capacitor: Capacitor App appeared first on Devdactic.

How to use Ionic In App Purchase with Capacitor

$
0
0

In app purchases can earn you a decent income from your mobile apps, and with Ionic and Capacitor it’s quite easy to setup everything and start earning!

In this post we will use the version two of the in app purchase plugin and make everything work with Capacitor!

Before we get started with our App, there are actually a few things we need to configure for our native apps.

iOS App Setup

Let’s start with iOS since it’s this time actually easier and faster. First of all you need an app identifier – perhaps you already have an app then you can skip it, otherwise go ahead and create a new one inside your developer account.

iap-ios-app-id

For this identifier we now also need to create an app inside App Store connect, which you might have done if your app is live already. Otherwise, go ahead and create a new app using the bundle identifier you specified in the first step.

Now within your app the important part begins: You need to set up products that can be purchased.

These products could be from 4 different categories like in the image below.
itc-iap-categories

Today we will focus on the first 2 categories, but using a subscription isn’t too complicated as well and mostly works the same once we get to the implementation.

Go ahead and create 2 products, you can pick the same ID for them like I did so you code will also work nicely afterwards.

To later test everything, you should also add test users within App Store connect and then log in with them on your iOS device for testing the app later.

This is actually enough for iOS, the products will be available while testing our app and you don’t have to wait or perform any other specific steps.

Android App Setup

For Android, we actually need to upload a signed APK to test everything, so a bit more work upfront.

This means, you have to create a new app (or use your existing) inside the Google Play developer console, and fill out all the required information in order to actually upload a first version.

Once you are done with these things, you can start and add products, just like we did for iOS before! I recommend to use the same ID for the products, while the other information like description and pricing can be totally different if you want to.

Besides that, you also might want to create a gmail test account which you can later add as a tester to your app since it’s enough to put our app into the alpha/beta track to test the purchase!

Because you don’t want to pay real money while testing, you should put every tester email into the Licence Test section which you can find in the general settings for your Google Play developer console!

iap-licence-testing

If the email is listed in here and the account is used on an Android device, you will see a notice that it’s a dummy transaction while testing the app!

Apparently, the products we added will only be available after like 6 hours or more, and we also need a real app build to test everything so let’s create our app now.

Ionic In App Purchase Implementation

First of all we can create our new app using the ID we used to create our iOS app, which will also be the bundle identifier for Android (which was set in your config.xml with Cordova in the past).

We can install the plugins we need directly with NPM since we don’t use Cordova, and afterwards add our native platforms after a first dummy build:

ionic start devdacticPurchase blank --type=angular --capacitor --package-id=com.devdactic.iap
cd ./devdacticPurchase

npm install @ionic-native/in-app-purchase-2
npm install cordova-plugin-purchase

ionic build

npx cap add ios
npx cap add android

In the past the Cordova plugin was added with a billing key for Android, but from my understanding after going through this issue it’s not really mandatory anymore and we don’t need to manually add any entry.

Like always with Ionic Native plugins we need to add them to our app/app.module.ts:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { InAppPurchase2 } from '@ionic-native/in-app-purchase-2/ngx';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    InAppPurchase2
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now we can use the plugin and we need to cover a few things:

  • Register our products: We need to register them by using their ID, so hopefully you used the same ID for the IAP for both iOS and Android!
  • Setup listeners: The plugin emits some events to which we can/should liste. These show the state of a product, and I recommend to check out the life-cycle diagram which shows the flow of states nicely!
  • Fetch products: We can’t just show hard coded data for products, that will get you a rejection. You need to fetch the real data from iOS/Android and present it, which the plugin can do for us

The only thing we have hard coded are the IDs of the products so we can register them locally and then fetch the real information for them, which we will display the user later based on the products array.

Our listeners takes care of handling a purchase, so if a product moves into the approved state we need to perform any action that’s related to the purchase. Once we call verify and finish, the product state will change again:

  • Consumables will go back to valid – they can be purchased again
  • Non consumables will move to owned, and you will always get the owned state when you start the app and refresh the store again

In our case we simply perform some dummy operation like increasing the gems count or changing the boolean of a pro version.

Although the store should refresh on start, showing a restore button is sometimes a requirement. You can easily call the refresh in there again, and your handlers will retrieve all the information about your products once more.

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

import { Component, ChangeDetectorRef } from '@angular/core';
import { Platform, AlertController } from '@ionic/angular';
import { InAppPurchase2, IAPProduct } from '@ionic-native/in-app-purchase-2/ngx';

const PRODUCT_GEMS_KEY = 'devgems100';
const PRODUCT_PRO_KEY = 'devpro';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  gems = 0;
  isPro = false;
  products: IAPProduct[] = [];

  constructor(private plt: Platform, private store: InAppPurchase2, private alertController: AlertController, private ref: ChangeDetectorRef) {
    this.plt.ready().then(() => {
      // Only for debugging!
      this.store.verbosity = this.store.DEBUG;

      this.registerProducts();
      this.setupListeners();
      
      // Get the real product information
      this.store.ready(() => {
        this.products = this.store.products;
        this.ref.detectChanges();
      });
    });
  }

  registerProducts() {
    this.store.register({
      id: PRODUCT_GEMS_KEY,
      type: this.store.CONSUMABLE,
    });

    this.store.register({
      id: PRODUCT_PRO_KEY,
      type: this.store.NON_CONSUMABLE,
    });

    this.store.refresh();
  }

  setupListeners() {
    // General query to all products
    this.store.when('product')
      .approved((p: IAPProduct) => {
        // Handle the product deliverable
        if (p.id === PRODUCT_PRO_KEY) {
          this.isPro = true;
        } else if (p.id === PRODUCT_GEMS_KEY) {
          this.gems += 100;
        }
        this.ref.detectChanges();

        return p.verify();
      })
      .verified((p: IAPProduct) => p.finish());


    // Specific query for one ID
    this.store.when(PRODUCT_PRO_KEY).owned((p: IAPProduct) => {
      this.isPro = true;
    });
  }

  purchase(product: IAPProduct) {
    this.store.order(product).then(p => {
      // Purchase in progress!
    }, e => {
      this.presentAlert('Failed', `Failed to purchase: ${e}`);
    });
  }

  // To comply with AppStore rules
  restore() {
    this.store.refresh();
  }

  async presentAlert(header, message) {
    const alert = await this.alertController.create({
      header,
      message,
      buttons: ['OK']
    });

    await alert.present();
  }

}

Now we should have an array of products and can display all information based on this – remember, these are our fetched products which contain exactly the price, description and everything we configured for our apps in the respective portals!

Quickly bring up your home/home.page.html and change it to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Devdactic IAP
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-list>
    <ion-item button *ngFor="let p of products" (click)="purchase(p)" detail="false">
      <ion-label class="ion-text-wrap">
        {{ p.title }}
        <p>{{ p.description }}</p>
      </ion-label>
      <ion-button slot="end">
        {{ p.price }} {{ p.currency }}
      </ion-button>
    </ion-item>
  </ion-list>

  <ion-button expand="full" (click)="restore()">Restore</ion-button>

  <ion-item>
    Gems: {{ gems }}
  </ion-item>
  <ion-item>
    Is Pro: {{ isPro }}
  </ion-item>
</ion-content>

Now the app is ready, and you can already test everything on your iOS device. But let’s also take the final step for Android.

Android Release Build with Capacitor

There’s no built in Capacitor command to build an APK, but by using gradlew (the native Android tooling) it becomes pretty easy.

First of all you need a release key for your app. If you don’t already have one, simply create a new one by running:

keytool -genkey -v -keystore android-release.keystore -alias release -keyalg RSA -keysize 2048 -validity 10000

If you want an almost automatic build, you can now move that file to android/app/android-release.keystore and configure gradle so it picks up the file and signs your APK on the fly!

To do so, simply add the following lines to your android/gradle.properties:

RELEASE_STORE_FILE=./android-release.keystore
RELEASE_STORE_PASSWORD=xxx
RELEASE_KEY_ALIAS=xxx
RELEASE_KEY_PASSWORD=xxx

Make sure to use the password you used when creating your release key, and the alias will be “release” if you followed my method. Otherwise use your existing key and credentials!

Note: There are more secure ways to keep your signing credentials confident like reading from a file that you don’t commit to Git, but for now we can use this simple local approach.

Now we just need to tell the gradle build to use our signing config when the app is build, and to read the keystore file from the right path.

All of this can be changed right inside the first part of the android/app/build.gradle:

android {
    signingConfigs {
        release {
            storeFile file(RELEASE_STORE_FILE)
            storePassword RELEASE_STORE_PASSWORD
            keyAlias RELEASE_KEY_ALIAS
            keyPassword RELEASE_KEY_PASSWORD

            // Optional, specify signing versions used
            v1SigningEnabled true
            v2SigningEnabled true
        }
    }

    compileSdkVersion rootProject.ext.compileSdkVersion
    defaultConfig {
        applicationId "com.devdactic.iap"
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
    }
}

Note: Make sure to use your own bundle ID or exchange it after you copy this code! Also, if you want to upload a new build you can simply increase the versionCode in here!

That’s the beauty of Capacitor once again – we can tweak the native project build and commit these files, and the build will work exactly like this in the future without the fear that this code will be lost!

Finally we need to add the billing permission to the android/app/src/main/AndroidManifest.xml which usually the Cordova plugin automatically handles. Simply go ahead and add an entry at the bottom where you can also see the Capacitor default permissions:

<uses-permission android:name="com.android.vending.BILLING" />

The final step is to build our APK, which we can do by navigating into the android folder and then simply running the gradle wrapper:

./gradlew assembleRelease

If everything works correctly you should find your signed APK at android/app/build/outputs/apk/release/app-release.apk!

Go ahead and upload this file in your Play console and move it to the alpha/beta track for testing, where you also add your test emails.

Important: If you are using a new app like I did, you need to wait until this build and the whole app is published. This doesn’t mean it’s live, but before this state, it’s still checked by Google and you won’t see any products in your app.

android.published-state

Conclusion

Configuring in app purchases isn’t too hard, while the testing can be tricky and confusing. Especially for Android you have to be patient until the app is approved and live, and you won’t be able to test anything before that point.

It made basically no difference to our app that we are using Capacitor, and we were even able to build a simple automatic release build by directly configuring our native projects!

You can also find more explanation while I talk about the whole process in the video below.

The post How to use Ionic In App Purchase with Capacitor appeared first on Devdactic.

How to Build an Ionic 5 Calendar with Modal & Customisation

$
0
0

Having a calendar component in your Ionic app could be an essential element for your whole app experience, and while there’s not a standard solution, there are a lot of free components we can use.

In this post we will use one of the best calendar components for Ionic called ionic2-calendar. Don’t worry about the name – it was updated for all versions and works fine with Ionic 5 as well!

We will integrate the component into our app and also create a simple modal so you could use the calendar sort of like a date picker as well!

Getting Started

First of all we need a new Ionic app and install the calendar. We also generate another page that we will later use for our modal:

ionic start ionCalendar blank --type=angular
cd ./ionCalendar
npm install ionic2-calendar
ionic g page pages/calModal

To use the calendar, you need to import it into the according page module. If you also want to localisee it for a specific language, you can use the registerLocaleData from Angular and import the language package you need. In my example I simply used the German language, but there are many more available!

Go ahead and change your home/home-module.ts to this now:

import { NgModule, LOCALE_ID } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { HomePage } from './home.page';

import { HomePageRoutingModule } from './home-routing.module';

import { NgCalendarModule  } from 'ionic2-calendar';
import { CalModalPageModule } from '../pages/cal-modal/cal-modal.module';

import { registerLocaleData } from '@angular/common';
import localeDe from '@angular/common/locales/de';
registerLocaleData(localeDe);

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    HomePageRoutingModule,
    NgCalendarModule,
    CalModalPageModule
  ],
  declarations: [HomePage],
  providers: [
    { provide: LOCALE_ID, useValue: 'de-DE' }
  ]
})
export class HomePageModule {}

That’s the only thing you need to do upfront for your calendar component!

Creating a Basic Calendar

To setup the calendar you need an array of events (which you could e.g. load from your API) and some basic settings for the calendar like the mode and the currentDate, which basically marks today in your calendar.

The mode specifies the view of the calendar, and you can select between month, week and day view.

Let’s go through the basic functions we add to our class:

  • next/back: Move the calendar view to the next month/week/day based on the current mode using the viewchild we added
  • onViewTitleChanged: Will be called by the calendar and reflects the current title of your view. This could also be customised with a special formatter as well!
  • onEventSelected: Called when you click on an entry in the calendar. In our case we simply present an alert with the information of the event.
  • createRandomEvents: Fill the calendar with some dummy data for testing, copied from the demo code.
  • removeEvents: Removes all events

You can either completely set the eventSource array to a new value, or otherwise also push single events to the source. We will see how the second part later in combination with our modal.

For now though go ahead and change the home/home.page.ts to:

import { CalendarComponent } from 'ionic2-calendar/calendar';
import { Component, ViewChild, OnInit, Inject, LOCALE_ID } from '@angular/core';
import { AlertController, ModalController } from '@ionic/angular';
import { formatDate } from '@angular/common';
import { CalModalPage } from '../pages/cal-modal/cal-modal.page';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage implements OnInit {
  eventSource = [];
  viewTitle: string;

  calendar = {
    mode: 'month',
    currentDate: new Date(),
  };

  selectedDate: Date;

  @ViewChild(CalendarComponent) myCal: CalendarComponent;

  constructor(
    private alertCtrl: AlertController,
    @Inject(LOCALE_ID) private locale: string,
    private modalCtrl: ModalController
  ) {}

  ngOnInit() {}

  // Change current month/week/day
  next() {
    this.myCal.slideNext();
  }

  back() {
    this.myCal.slidePrev();
  }

  // Selected date reange and hence title changed
  onViewTitleChanged(title) {
    this.viewTitle = title;
  }

  // Calendar event was clicked
  async onEventSelected(event) {
    // Use Angular date pipe for conversion
    let start = formatDate(event.startTime, 'medium', this.locale);
    let end = formatDate(event.endTime, 'medium', this.locale);

    const alert = await this.alertCtrl.create({
      header: event.title,
      subHeader: event.desc,
      message: 'From: ' + start + '<br><br>To: ' + end,
      buttons: ['OK'],
    });
    alert.present();
  }

  createRandomEvents() {
    var events = [];
    for (var i = 0; i < 50; i += 1) {
      var date = new Date();
      var eventType = Math.floor(Math.random() * 2);
      var startDay = Math.floor(Math.random() * 90) - 45;
      var endDay = Math.floor(Math.random() * 2) + startDay;
      var startTime;
      var endTime;
      if (eventType === 0) {
        startTime = new Date(
          Date.UTC(
            date.getUTCFullYear(),
            date.getUTCMonth(),
            date.getUTCDate() + startDay
          )
        );
        if (endDay === startDay) {
          endDay += 1;
        }
        endTime = new Date(
          Date.UTC(
            date.getUTCFullYear(),
            date.getUTCMonth(),
            date.getUTCDate() + endDay
          )
        );
        events.push({
          title: 'All Day - ' + i,
          startTime: startTime,
          endTime: endTime,
          allDay: true,
        });
      } else {
        var startMinute = Math.floor(Math.random() * 24 * 60);
        var endMinute = Math.floor(Math.random() * 180) + startMinute;
        startTime = new Date(
          date.getFullYear(),
          date.getMonth(),
          date.getDate() + startDay,
          0,
          date.getMinutes() + startMinute
        );
        endTime = new Date(
          date.getFullYear(),
          date.getMonth(),
          date.getDate() + endDay,
          0,
          date.getMinutes() + endMinute
        );
        events.push({
          title: 'Event - ' + i,
          startTime: startTime,
          endTime: endTime,
          allDay: false,
        });
      }
    }
    this.eventSource = events;
  }

  removeEvents() {
    this.eventSource = [];
  }

}

With that in place we are ready for a basic view of our calendar.

We add some buttons to later trigger our modal and also the dummy functions to fill and clear the array of events.

Besides that we can use a cool segment from Ionic to easily switch between the three different calendar modes, which works by simply changing the mode property!

Additionally we add the helper functions to move left and right in the calendar (which also simply works by sliding the view!) and in the center the title of the current view that we see, which will be updated once we change to another month (or week, or day..).

The calendar itself has a lot of properties that we can or should set. The ones we use are:

  • eventSource: The array of events which will be displayed
  • calendarMode: The view mode as described before
  • currentDate: The today date
  • onEventSelected: Called when we click on a day with event
  • onTitleChanged: Triggered when we change the view
  • start/step: Additional settings for the different views

Now we can go ahead with our home/home.page.html and change it to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic Calendar
    </ion-title>
    <ion-buttons slot="end">
      <ion-button (click)="openCalModal()">
        <ion-icon name="add" slot="icon-only"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-segment [(ngModel)]="calendar.mode">
    <ion-segment-button value="month">
      <ion-label>Month</ion-label>
    </ion-segment-button>
    <ion-segment-button value="week">
      <ion-label>Week</ion-label>
    </ion-segment-button>
    <ion-segment-button value="day">
      <ion-label>Day</ion-label>
    </ion-segment-button>
  </ion-segment>

  <ion-row>
    <ion-col size="6">
      <ion-button (click)="createRandomEvents()" expand="block" fill="outline">
        Add random events
      </ion-button>
    </ion-col>
    <ion-col size="6">
      <ion-button (click)="removeEvents()" expand="block" fill="outline">
        Remove all events
      </ion-button>
    </ion-col>
  </ion-row>

  <ion-row>
    <!-- Move back one screen of the slides -->
    <ion-col size="2">
      <ion-button fill="clear" (click)="back()">
        <ion-icon name="arrow-back" slot="icon-only"></ion-icon>
      </ion-button>
    </ion-col>

    <ion-col size="8" class="ion-text-center">
      <h2>{{ viewTitle }}</h2>
    </ion-col>

    <!-- Move forward one screen of the slides -->
    <ion-col size="2">
      <ion-button fill="clear" (click)="next()">
        <ion-icon name="arrow-forward" slot="icon-only"></ion-icon>
      </ion-button>
    </ion-col>
  </ion-row>

  <calendar
    [eventSource]="eventSource"
    [calendarMode]="calendar.mode"
    [currentDate]="calendar.currentDate"
    (onEventSelected)="onEventSelected($event)"
    (onTitleChanged)="onViewTitleChanged($event)"
    startHour="6"
    endHour="20"
    step="30"
    startingDayWeek="1"
  >
  </calendar>

</ion-content>

The standard calendar should work now, and you can also see how it looks per default with some random events!

Adding Events with Calendar Modal

Although the component is meant for something else, I thought it might look cool inside a custom modal as some sort of date picker – and it does!

Therefore we will add a new function to our page that presents our modal and catches the onDidDismiss event, in which we might get back a new event that we then add to our eventSource.

Here we can see the different of pushing an event – we also need to call loadEvents() on the calendar component in order to update the data!

Since we assume that we add an all-day event in our example, I manually set the end time to one day later here. Feel free to use custom times or add another ion-datetime to the modal in your example to pick an exact time!

Now go ahead and change the home/home.page.ts to:

async openCalModal() {
  const modal = await this.modalCtrl.create({
    component: CalModalPage,
    cssClass: 'cal-modal',
    backdropDismiss: false
  });

  await modal.present();

  modal.onDidDismiss().then((result) => {
    if (result.data && result.data.event) {
      let event = result.data.event;
      if (event.allDay) {
        let start = event.startTime;
        event.startTime = new Date(
          Date.UTC(
            start.getUTCFullYear(),
            start.getUTCMonth(),
            start.getUTCDate()
          )
        );
        event.endTime = new Date(
          Date.UTC(
            start.getUTCFullYear(),
            start.getUTCMonth(),
            start.getUTCDate() + 1
          )
        );
      }
      this.eventSource.push(result.data.event);
      this.myCal.loadEvents();
    }
  });
}

We’ve also added a custom class to the modal to make it stand out more like an overlay. To use the styling, we need to declare it inside the global.scss like this:

.cal-modal {
    --height: 80%;
    --border-radius: 10px;
    padding: 25px;
}

Now we got a nice looking modal and only need some more input fields and the calendar component once again.

Like before, we need to add it first of all to the module file, so in our case change the cal-modal/cal-modal.module.ts to:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

import { IonicModule } from '@ionic/angular';

import { CalModalPageRoutingModule } from './cal-modal-routing.module';

import { CalModalPage } from './cal-modal.page';

import { NgCalendarModule  } from 'ionic2-calendar';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    CalModalPageRoutingModule,
    NgCalendarModule
  ],
  declarations: [CalModalPage]
})
export class CalModalPageModule {}

Since we now don’t want to display any events, we need only a handful of functions.

We need an empty event object that will be passed back to our initial page inside the dismiss() function of the modal, and we need to react to the view title changes and click events on a time slot.

To store it correctly, we also need to transform the data we get inside the onTimeSelected() to a real date object!

Now open the cal-modal/cal-modal-page.ts and change it to:

import { Component, AfterViewInit } from '@angular/core';
import { ModalController } from '@ionic/angular';

@Component({
  selector: 'app-cal-modal',
  templateUrl: './cal-modal.page.html',
  styleUrls: ['./cal-modal.page.scss'],
})
export class CalModalPage implements AfterViewInit {
  calendar = {
    mode: 'month',
    currentDate: new Date()
  };
  viewTitle: string;
  
  event = {
    title: '',
    desc: '',
    startTime: null,
    endTime: '',
    allDay: true
  };

  modalReady = false;

  constructor(private modalCtrl: ModalController) { }

  ngAfterViewInit() {
    setTimeout(() => {
      this.modalReady = true;      
    }, 0);
  }

  save() {    
    this.modalCtrl.dismiss({event: this.event})
  }

  onViewTitleChanged(title) {
    this.viewTitle = title;
  }

  onTimeSelected(ev) {    
    this.event.startTime = new Date(ev.selectedTime);
  }

  close() {
    this.modalCtrl.dismiss();
  }
}

The last missing piece is the modal view with some items as input fields and the calendar component just like before, but this time without any events!

You might have noticed the strange modalReady variable already. I couldn’t make the calendar work correctly when deployed to a device, since I assume the view wasn’t rendered correctly and something internally broke.

By hiding it until the view has finished loading we can easily fix this, and in reality you actually won’t even notice it!

Warp up the implementation with the code for the cal-modal/cal-modal.page.html:

<ion-toolbar color="primary">
  <ion-buttons slot="start">
    <ion-button (click)="close()">
      <ion-icon name="close" slot="icon-only"></ion-icon>
    </ion-button>
  </ion-buttons>
  <ion-title>{{ viewTitle }}</ion-title>
  <ion-buttons slot="end">
    <ion-button (click)="save()">
      <ion-icon name="checkmark" slot="icon-only"></ion-icon>
    </ion-button>
  </ion-buttons>
</ion-toolbar>

<ion-content>
  <ion-item>
    <ion-label position="stacked">Title</ion-label>
    <ion-input tpye="text" [(ngModel)]="event.title"></ion-input>
  </ion-item>
  <ion-item>
    <ion-label position="stacked">Description</ion-label>
    <ion-input tpye="text" [(ngModel)]="event.desc"></ion-input>
  </ion-item>

  <calendar
    *ngIf="modalReady"
    [calendarMode]="calendar.mode"
    [currentDate]="calendar.currentDate"
    (onTitleChanged)="onViewTitleChanged($event)"
    (onTimeSelected)="onTimeSelected($event)"
    lockSwipes="true"
  >
  </calendar>
</ion-content>

Now you are able to open the modal, set a date and some information and our initial page will create the event and add it to the source for the calendar!

Customising the Calendar View

Because the last time we used the component a lot of you asked about customisation, let’s add a few more things today.

First, we might wanna get rid of the list below the month view in our modal, since the calendar has no events in that place anyway.

There’s no setting to hide it (at least I didn’t find it) but we can hide the list by using the correct CSS class and access the encapsulated calendar component like this in our cal-modal/cal-modal.page.scss:

:host ::ng-deep {
    .monthview-container {
        height: auto !important;
    }

    .event-detail-container {
        display: none;
    }
}

Using CSS is one way to customise it, and another great way to change the appearance of the calendar is using custom templates for specific parts of the calendar.

Let’s for example change the look and color of a cell inside the calendar so it has a more rounded, different color and also shows the number of events on that specific date.

We could do this by creating a template and passing it to the monthviewDisplayEventTemplate property of the calendar like this inside the home/home.page.html:

<calendar
  [eventSource]="eventSource"
  [calendarMode]="calendar.mode"
  [currentDate]="calendar.currentDate"
  (onEventSelected)="onEventSelected($event)"
  (onTitleChanged)="onViewTitleChanged($event)"
  startHour="6"
  endHour="20"
  step="30"
  startingDayWeek="1"
  [monthviewDisplayEventTemplate]="template"
>
</calendar>

<ng-template #template let-view="view" let-row="row" let-col="col">
  <div [class.with-event]="view.dates[row*7+col].events.length">
    {{view.dates[row*7+col].label}}
    <div class="indicator-container">
      <div class="event-indicator" *ngFor="let e of view.dates[row*7+col].events"></div>
    </div>
  </div>
</ng-template>

As you can see we also added our own CSS rules in there, which we could now define either for our own template or additionally some rules for the calendar itself (following the style like we did in the modal) right inside the home/home.page.scss:

.indicator-container {
  padding-left: 0.5rem;
  padding-bottom: 0.4rem;
}

.event-indicator {
  background: var(--ion-color-success);
  width: 5px;
  height: 5px;
  border-radius: 5px;
  display: table-cell;
}

:host ::ng-deep {
  .monthview-primary-with-event {
    background-color: white !important;
  }

  .monthview-selected {
    background-color: var(--ion-color-success) !important;
  }
}

.with-event {
  background-color: var(--ion-color-primary);
  border-radius: 15px;
}

Now the view looks a lot different, and this hopefully gives you an idea how you can customise basically every part of this great component!

Conclusion

There are not a lot of components that exist since years and get updates for all major Ionic versions, but this calendar is one of them. Kudos again to the creator!

If you are looking for an alternative, also check out my post on the Angular calendar.

You can also find a video version of this tutorial below.

The post How to Build an Ionic 5 Calendar with Modal & Customisation appeared first on Devdactic.

Viewing all 183 articles
Browse latest View live