Menu hamburger icon

Creating a Pokémon hybrid app with Angular 2 and Onsen UI

Angular 2 Pokedex App

A couple of months ago we published an article on how to create a Pokedex application using Angular 2. Since then, the stable version of Angular 2 has been released and we have released the RC version of our Angular 2 bindings.

In this article we will use Onsen UI 2 and Angular 2 to create a simple mobile Pokedex app. We will use some of the code from the previous article, but we will integrate it with Onsen UI. We will also take a look at the Monaca CLI, which is a great tool for creating, debugging, and developing apps with Angular 2 and Onsen UI.

With the Monaca CLI you get access to several templates that can serve as starting points for your own apps. The CLI is using Webpack to build and package your app so you don’t have to set it up yourself. The Monaca CLI also integrates with the Monaca Debugger so you can test and debug your apps in real devices.

This app is using the great PokéAPI. The API contains all the necessary information to create a complete Pokedex app, with information from all the Pokemon games. Since this is just a sample app, we will only use a small part of the information available.

You can try out the app below:

The source code for the app is available in this GitHub repo.

Installing the Monaca CLI

This app was created using the Monaca CLI. It’s a command line tool that can be used to create, debug and build hybrid apps. It is available on NPM so you can install it with the following command:

[sudo] npm install -g monaca

Or, if you are using yarn you can install it like this:

yarn global add monaca

When the program is successfully installed, you can run monaca -v to see the current version:

$ monaca -v
2.1.6

Creating an application

Creating an app with the Monaca CLI is very simple. All you need to do is to navigate to the directory where you want to create your app and run:

monaca create my-app

You can now see what kind of templates are available.

monaca create abc
? Choose a category: 
  Onsen UI 
  Onsen UI and Angular 1 
❯ Onsen UI and Angular 2 
  Onsen UI and React 
  Ionic 
  No Framework 
  Sample Apps

Since we are creating an app with Angular 2 and Onsen UI we will select that and press enter.

? Select a template - Press P to see a preview (Use arrow keys: ↑ ↓ ← →)
❯ Onsen UI V2 Angular 2 Tabbar 
  Onsen UI V2 Angular 2 Splitter 
  Onsen UI V2 Angular 2 Navigation 
  Onsen UI V2 Angular 2 Minimum

In the next screen we select what kind of navigation pattern the app will use. Here we can create an app with tabs, a swipeable side menu or stack navigation. The app we are creating will use stack navigation so we select the Onsen UI V2 Angular 2 Navigation template.

If we press enter again the application will be created for us and all the dependencies will be installed. This might take a while.

Now enter the directory:

cd my-app

To preview the app you can use the following command:

monaca preview

This will start a web server. The app will be automatically rebuilt every time a change occurs so it’s recommended to always have this server running so we can see the changes instantly.

Creating the app

We will put all our code in the src/app folder. This is where the Angular 2 components, services, etc. are located.

The Monaca CLI has created a very simple app for us where we can push and pop pages from the stack. This stack navigation is made possible with the <ons-navigator> component which is rendered by the MyApp component in the app.ts file.

We need to add some behavior to this component. This component will render the master page of the app which contains a list of Pokemon fetched from the PokeAPI. When a Pokemon is clicked we will push a new component called DetailPage to the page stack that displays some additional information about the Pokemon.

The ngOnInit method is called when the component is initialized and we use it to load the first 20 Pokemon from the API using the loadMore() method. This method is also called when the user clicks a button at the bottom of the list.

In the constructor method two services are injected. The PokedexService is used to fetch information from the Pokemon API and the CaughtPokemonService is a simple service that keeps track of the Pokemon that have been caught using a Set object.

To push a page to the stack the ViewChild decorator is used to get a reference to the OnsNavigator directive.

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

import {OnsNavigator} from 'angular2-onsenui';
import {PokedexService} from './pokedex.service';
import {CaughtPokemonService} from './caught-pokemon.service';
import {Pokemon} from './pokemon';
import {DetailPage} from './detail';

@Component({
  selector: 'app',
  template: require('./app.html'),
  styles: [require('./app.css')]
})
export class MyApp implements OnInit {
  counter: number;
  pokemon: Pokemon[] = [];
  isLoading: boolean = false;
  error: boolean = false;

  @ViewChild('navi')
  private navi;

  constructor(
    private pokedexService: PokedexService,
    private caughtPokemon: CaughtPokemonService
  ) {
  }

  ngOnInit() {
    this.loadMore();
  }

  loadMore() {
    if (this.isLoading) {
      return;
    }

    this.isLoading = true;

    this.pokedexService.getPokemon(this.pokemon.length, 20)
      .then(pokemon => {
        this.pokemon = this.pokemon.concat(pokemon);
        this.isLoading = false;
        this.error = false;
      })
      .catch(() => {
        this.error = true;
        this.isLoading = false;
      });
  }

  push(pokemon: Pokemon) {
    const data = {
      pokemon
    };

    this.navi.nativeElement.pushPage(
      DetailPage,
      {data}
    );
  }
}

The template in app.html looks like this:

<ons-navigator #navi>
  <ons-page>
    <ons-toolbar>
      <div class="center">Pokedex</div>
    </ons-toolbar>

    <div class="page__background"></div>
    <div class="page__content">
      <ons-list>
        <ons-list-item
          tappable
          modifier="chevron"
          *ngFor="let p of pokemon"
          (click)="push(p)"
        >
          <div class="left">
            <img class="pokemon-icon" src="/images/pokemon/icons/{{ p.id }}.png">
          </div>
          <div class="center">
            #{{ p.id }}
            {{ p.name | capitalize }}
          </div>
          <div class="right">
            <img
              class="pokeball"
              src="/images/pokemon/pokeball.png"
              *ngIf="caughtPokemon.has(p.id)"
            >
          </div>
        </ons-list-item>
      </ons-list>

      <ons-button
        modifier="large quiet"
        (disabled)="isLoading"
        (click)="loadMore()"
      >
        <span [hidden]="isLoading">
          Load more
        </span>
        <span [hidden]="!isLoading">
          Loading...
        </span>
      </ons-button>
    </div>
  </ons-page>
</ons-navigator>

What this does is that it will render the MasterPage component as the first page in the navigator stack.

The DetailPage component

The component that displays the details for a Pokemon is also quite simple.

One important note is how the parameters that were passed in the push() method of the MasterPage component are retrieved. This is done by injecting the Params service in the constructor:

constructor(
    private navi: OnsNavigator,
    private params: Params,
    private pokedexService: PokedexService,
    private caughtPokemon: CaughtPokemonService
) {
    /**
     * Get the Pokemon that was pushed.
     */
    this.pokemon = params.data.pokemon;
}

This is the code for the whole component. It can be found in the detail.ts file.

import {Component} from '@angular/core';
import {OnsNavigator, Params} from 'angular2-onsenui';

import {PokedexService} from './pokedex.service';
import {CaughtPokemonService} from './caught-pokemon.service';

import {Pokemon} from './pokemon';
import {PokemonDetails} from './pokemon-details';

@Component({
  selector: 'ons-page[page]',
  template: require('./detail.html'),
  styles: [require('./detail.css')]
})
export class DetailPage {
  pokemon: Pokemon;
  details: PokemonDetails;
  loaded: boolean = false;

  constructor(
    private navi: OnsNavigator,
    private params: Params,
    private pokedexService: PokedexService,
    private caughtPokemon: CaughtPokemonService
  ) {
    /**
     * Get the Pokemon that was pushed.
     */
    this.pokemon = params.data.pokemon;
  }

  onChange(e, pokemon) {
    if (e.target.checked) {
      this.caughtPokemon.add(pokemon.id);
    } else {
      this.caughtPokemon.remove(pokemon.id);
    }
  }

  ngOnInit() {
    this.pokedexService.getPokemonDetails(this.pokemon.id)
      .then(details => {
        this.loaded = true;
        this.details = details;
      });
  }
}

The template will display some information about the Pokemon and a checkbox that will call the onChange() method when toggled. The template code is in the detail.html file:

<ons-toolbar>
  <div class="left">
    <ons-back-button>Back</ons-back-button>
  </div>
  <div class="center">{{ pokemon.name | capitalize }}</div>
</ons-toolbar>

<div class="content">
  <img class="sprite" src="/images/pokemon/sprites/{{ pokemon.id }}.png">

  <div
    class="progress"
    *ngIf="!loaded"
  >
    <ons-progress-circular
      indeterminate
    >
    </ons-progress-circular>
  </div>

  <div class="info" *ngIf="loaded">
    <div class="types">
      <div
        class="type type-{{ type }}"
        *ngFor="let type of details.types">
        {{ type }}
      </div>
    </div>


    <div class="stats">
      <div class="stat">
        <div class="stat-label">
          Height
        </div>
        <div class="stat-value">
          {{ details.height / 10 }}m
        </div>
      </div>
      <div class="stat">
        <div class="stat-label">
          Weight
        </div>
        <div class="stat-value">
          {{ details.weight / 10 }}kg
        </div>
      </div>
    </div>

    <div class="description">
      {{ details.description }}
    </div>
  </div>
</div>

<ons-bottom-toolbar>
  <div class="caught">
    <div class="caught-label">
      Caught
    </div>
    <div class="caught-checkbox">
      <ons-input
      type="checkbox"
      (change)="onChange($event, pokemon)"
      [checked]="caughtPokemon.has(pokemon.id)"
      ></ons-input>
    </div>
  </div>
</ons-bottom-toolbar>

Services

A service is an object that can be shared among all your components. It is often used to fetch and store data. In our app, we will create a service that fetches data from the Pokémon API as well as a service that maintains a list of all the Pokemon that have been caught.

The CaughtPokemonService uses the Set data structure that was added to JavaScript in ES6. Angular 2 apps are written in TypeScript so we can use all ES6 features since they are transpiled to ES5.

The CaughtPokemonService code is in the caught-pokemon.service.ts file:

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

@Injectable()
export class CaughtPokemonService {
  constructor() {
    if (window.localStorage.getItem('caught-pokemon')) {
      const c = JSON.parse(window.localStorage.getItem('caught-pokemon'));
      c.forEach((x) => this.caught.add(x));
    }
  }

  update() {
    const json = JSON.stringify(this.caught);
    window.localStorage.setItem('caught-pokemon', json);
  }

  caught: Set<number> = new Set<number>();

  add(id: number) {
    this.caught.add(id);
    this.update();
  }

  remove(id: number) {
    this.caught.delete(id);
    this.update();
  }

  has(id): boolean {
    return this.caught.has(id);
  }

  get count(): number {
    return this.caught.size;
  }
}

In addition to maintaining a set of Pokemon IDs it will also save the list to localStorage so the app can remember even if it is restarted or if the page is reloaded.

We already created a simple PokedexService in the previous blog post. This time it has been extended to also fetch some additional information that is displayed on the details page:

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/toPromise';

import {Pokemon} from './pokemon';
import {PokemonDetails} from './pokemon-details';

@Injectable()
export class PokedexService {
  private baseUrl: string = 'https://pokeapi.co/api/v2/pokemon/';

  constructor(private http: Http) { }

  getPokemon(offset: number, limit: number): Promise<Pokemon> {
    return this.http.get(`${this.baseUrl}?offset=${offset}&limit=${limit}`)
      .toPromise()
      .then(response => response.json().results)
      .then(items => items.map((item, idx) => {
        const id: number = idx + offset + 1;
        return {
          name: item.name,
          id
        };
      }));
  }

  getPokemonDetails(id: number): Promise<PokemonDetails> {
    let tmp;

    return this.http.get(`${this.baseUrl}${id}/`)
      .toPromise()
      .then(response => response.json())
      .then(details => {
        const types = details.types
          .map(t => {
            return t.type.name
          });

        tmp = {
          id: details.id,
          name: details.name,
          weight: details.weight,
          height: details.height,
          types
        };

        /**
         * We need to make another call
         * to get the description text.
         */
        return this.http
          .get(details.species.url)
          .toPromise();
      })
      .then(response => response.json())
      .then(species => {
        /**
         * Find the latest English
         * description.
         */
        let description = '';
        const entries = species.flavor_text_entries;

        for (let i = 0; i < entries.length; i++) {
          const entry = entries[i];

          if (entry.language.name === 'en') {
            description = entry.flavor_text;
            break;
          }
        }

        tmp.description = description;
        return tmp;
      });
  }
}

Conclusion

In this article we have created a very simple Pokedex app using Angular 2 and Onsen UI 2. Even if this Pokedex app doesn’t display a lot of information, we hope you can see how it could be expanded into a more complete app by adding more pages to the navigator and adding more information from the PokeAPI. The Angular 2 bindings for Onsen UI are currently in RC stage but we are getting closer to a stable version.

Onsen UI is an open source library used to create the UI of hybrid apps. You can find more information on our GitHub page. If you like Onsen UI, please don’t forget to give us a star! ★★★★★

Comments