Chapter 8.Building a Tab-Based App
Avery common type of application you might build is one that uses a tab-based navigation system. This design pattern works very well when you have a limited number (five or fewer) of groups (or tabs) of content. We are going to use this design pattern to create an app that will allow you to explore the various United States National Parks (in honor of them celebrating their centennial in 2016).Figure 8-1shows what our final app will look like.
We are again going to use the Ionic CLI to scaffold our application. First, create a new directory that we will be building from. I named my directoryIonicParks:
$ ionic start IonicParks
Since we are going to be creating a tabs-based app, we did not need to pass in a template name, since the tab template is the default.
Next, change your working directory to the newly created_IonicParks_directory:
$ cd IonicParks
Now let’s explore the template itself (Figure 8-2) before we get down to business:
$ ionic serve
Figure 8-1.The completed Ionic national parks app
Figure 8-2.Ionic tabs template
Not a lot here, but we can navigate between the three tabs (named Home, About, and Contact), and see the content change.
Taking a look inside the_app.module.ts_file, we see that instead of importing one page, we now are importing four. These four pages comprise the three pages for each tab (HomePage, AboutPage, and ContactPage) and one page (TabsPage) that will serve as the container for the application. Each of these pages is included in the declaration andentryComponents
array.
Looking at the_app.component.ts_file, the only change here is that therootPage
is the TabsPage and not a specific tab view.
Now, let’s go take a look at thepages_directory. Inside this directory, we will find four additional directories:_about,contact,home, andtabs. Open the_home_directory, and within it are the HTML, SCSS, and TS files that define our home tab.
The SCSS file is just a placeholder, with no actual content inside. The HTML file has a bit more content inside.
First, the HTML defines an<ion-navbar>
and<ion-title>
component:
<
ion-header
>
<
ion-navbar
>
<
ion-title
>
Home
<
/ion-title
>
<
/ion-navbar
>
<
/ion-header
>
Next, the code defines the<ion-content>
tag. The component also sets thepadding
directive within the<ion-content>
tag:
<
ion-content padding
>
The rest of the HTML is just plain-vanilla HTML.
Now, let’s take a look at the component’s TypeScript file. This file is about as light-weight as possible for an Ionic Page:
import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
@Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
constructor(public navCtrl: NavController) {
}
}
OurComponent
module is imported into our code from the Angular core. Next, ourNavController
is imported from the ionic-angular library. This component is used whenever we need to navigate to another screen.
In ourComponent
decorator, we set ourtemplateURL
to the HTML file and the selector to page-home.
The last bit of code is just setting theHomePage
class to be exported and has the navController pass into the constructor.
The other two pages are almost identical to thisPage
component. Each exporting out a component respectively namedAboutPage
andContactPage
.
Let’s look at the tabs themselves. In the_tabs_directory, we see that it contains just a_tabs.html_file and a_tabs.ts_file. Let’s look at the HTML file first:
<
ion-tabs
>
<
ion-tab [root]="tab1Root" tabTitle="Home" tabIcon="home"
>
↵
<
/ion-tab
>
<
ion-tab [root]="tab2Root" tabTitle="About" tabIcon="information-circle"
>
↵
<
/ion-tab
>
<
ion-tab [root]="tab3Root" tabTitle="Contact" tabIcon="contacts"
>
↵
<
/ion-tab
>
<
/ion-tabs
>
Like Ionic 1, Ionic 2 also has an<ion-tabs>
and<ion-tab>
components. The<ion-tabs>
component is just a wrapper component for each of its children, the actual tabs themselves. The<ion-tab>
component has a more few attributes that we need to change. Let’s start with the two easier ones:tabTitle
andtabIcon
. These two attributes set the text label of the tab and the icon that will be displayed. The icon names are from the IonIcon library.
You do not need to set both the title and the icon on tab component. Depending on how you want yours to look, only include what you want.
Also, depending on the platform you are running your app on, the tab style and position will automatically adapt (seeFigure 8-3).
Figure 8-3.Tabs component rendering based on platform type
If you want to force a specific tab placement, there are two options—either directly on the component itself with:
<
ion-tabs tabsPlacement="top"
>
or globally through the app config options. This is done in the_app.module.ts_file:
IonicModule.forRoot(MyApp, {tabsPlacement: 'top'} )
For a full list of configuration options, check out theIonic Framework documentation.
The last item to look at in the<ion-tab>
is the[root]
binding that defines what component should act as that tab’s root:
import { Component } from '@angular/core';
import { HomePage } from '../home/home';
import { AboutPage } from '../about/about';
import { ContactPage } from '../contact/contact';
@Component({
templateUrl: 'tabs.html'
})
export class TabsPage {
// this tells the tabs component which Pages
// should be each tab's root Page
tab1Root: any = HomePage;
tab2Root: any = AboutPage;
tab3Root: any = ContactPage;
constructor() {
}
}
We have our now familiarimport
statements. The first loads the baseComponent
module, and the next three load each of the pages for our tabs.
Next, in ourComponent
decorator, we set thetemplateUrl
to the_tabs.html_file.
The class definition is where we assign each of the tabs to its corresponding components. That is all we need to establish our tabs framework. Ionic will manage the navigation state for each tab for us.
Bootstrapping Our App
Now that we have a general understanding of the structure of a tab-based Ionic application, we can start to modify it for our app. But rather than having you go through all the files and folders and rename them to something meaningful, we are going to take a shortcut. I have already made the initial changes to the template files. In addition, I also included a datafile with the national park data and various images for use in the app.
In earlier versions of the Ionic CLI, we were able to use external URLs as templates, but this feature was removed in version 3. From the GitHib repo (https://github.com/chrisgriffith/Ionic2Parks\, down the master branch. Once the code has downloaded, unzip the file, and then copy the src directory from the download, replacing the existing src directory in our project. If you runionic serve
again, there should now be two tabs, one named Parks and one named Map. They don’t have any content in them yet, so let’s fix that issue.
Loading Data via the HTTP Service
Before we start building the app in earnest, let’s create a provider to load our local JSON data into our app. This way, we will have actual data to work with as we build out our app.
In theapp_directory, create a new directory named_providers; and within that directory, create a new file namedpark-data.ts.
As you can expect, using the HTTP service in Angular is slightly different from Angular 1. The main difference is that Angular 2’s HTTP service returns Observables throughRxJS, whereas$http
in Angular 1 returns Promises.
Observables give us expanded flexibility when it comes to handling the responses coming from the HTTP requests. As an example, we could leverage an RxJS operator likeretry
so that failed HTTP requests are automatically re-sent. This can be very useful if our app has poor, weak, or intermittent network coverage. Since our data is being loaded locally, we don’t need to worry about that issue.
Returning to our_park-data.ts_file, we will inject three directives into it:
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
Next, we use the@Injectable()
decorator. The Angular 2 documentation reminds us to remember to include the parentheses after the@Injectable
; otherwise, your app will fail to compile.
We are going to defineParkData
as the provider’s class. Now add a variable nameddata
, and define its type toany
and set it to benull
. In the constructor, we will pass in theHttp
directive that we imported and classify it as a public variable. The class’s actual constructor is currently empty:
@Injectable()
export class ParkData {
data: any = null;
constructor(public http: Http) {}
}
Within the class definition, we will create a new method namedload
. This method will do the actual loading of our JSON data. In fact, we will add in some checks to make sure we only load this data once throughout the lifespan of our app. Here is the complete method:
load() {
if (this.data) {
return Promise.resolve(this.data);
}
return new Promise(resolve =
>
{
this.http.get('assets/data/data.json')
.map(res =
>
res.json())
.subscribe(data =
>
{
this.data = data;
resolve(this.data);
});
});
}
Since we are hard-coding the file’s location that we are loading, our method is not taking in a source location—hence,load()
.
The first thing this method does is check if the JSON data had been loaded and saved into thedata
variable. If it has, then we return a Promise that resolves to this saved data. We have to resolve our data to a Promise since that is the same return type that the actual loading portion uses inpark-list.ts. We will look at working with Observables in another chapter.
Let’s look at that block of code in detail. If the data has not been loaded, we return a basic Promise object. We set it to resolve after calling the HTTP service and using theget
method from our JSON data that is atassets/data/data.json. As with all Promises, we need to define its subscription. Again using the fat arrow function,=>, the result is stored in a variable namedres
. We will take the string and use Angular’s built-in JSON converter to parse it into an actual object. Finally, we will resolve our Promise with the actual data.
At this point, we only have written the provider. Now, we need to actually use it in our app. Openapp.component.ts, and addimport { ParkData } from '../providers/park-data';
with the rest of the import statements. This will load the provider and assign it toParkData
.
Next, we need to add an array of providers in the@Component
declaration:
@Component({
templateUrl: 'app.html',
providers: [ ParkData ]
})
Let’s now modify the constructor to load our data. We will pass in the reference to theParkData
provider into the constructor. After theplatform.ready
code block, we will call theparkData.load()
method. This will trigger the provider to load our datafile:
constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen,
public parkData: ParkData) {
platform.ready().then(() =
>
{
// Okay, so the platform is ready and our plugins are available.
// Here you can do any higher level native things you might need.
statusBar.styleDefault();
splashScreen.hide();
});
parkData.load();
}
If you want to do a quick test to see if the data is being loaded, wrap theparkData.load()
in aconsole.log()
. The__zone_symbol__value
of theZoneAwarePromise
will contain an array of the 59 objects that are created.
WHAT IS A ZONE?
A zone is a mechanism for intercepting and keeping track of asynchronous work. Since our data is being loaded in an asynchronous fashion, we need to use zones to keep track of the original context that we made our request in.
Here is a formated sample of what the each of the park’s object data looks like:
createDate: "October 1, 1890"
data: "Yosemite has towering cliffs, waterfalls, and sequoias in a diverse↵
area of geology and hydrology. Half Dome and El Capitan rise from the↵
central glacier-formed Yosemite Valley, as does Yosemite Falls, North↵
America's tallest waterfall. Three Giant Sequoia groves and vast ↵
wilderness are home to diverse wildlife."
distance: 0
id: 57
image: "yosemite.jpg"
lat: 37.83
long: -119.5
name: "Yosemite"
state: "California"
Display our Data
Now that the data has been read into the app and is available via our service provider, let’s turn our attention to actually displaying our 59 National Parks in a list.
First, we need to add a method on ourParkData
service provider to actually return the data that we have loaded. After theload
method, add the following method:
getParks() {
return this.load().then(data =
>
{
return data;
});
}
Two things to note about this method. First, it calls theload
method. This is a safety check to ensure that the data is there. If for some reason it is not, it will load it for us. Since our provider is using Promises, constructing a system that can be chained together is quite easy. Second, since this system is Promise based, we have to handle everything in a.then
syntax. This is something that you might have to remember to do as you migrate from Angular 1 to Angular.
Switching topark-list.html, we add the following after the<ion-content>
tag:
<
ion-list
>
<
ion-item *ngFor="let park of parks" ↵
(click)="goParkDetails(park)" detail-push
>
<
ion-thumbnail item-left
>
<
img src="assets/img/thumbs/{{park.image}}"
>
<
/ion-thumbnail
>
<
h2
>
{{park.name}}
<
/h2
>
<
p
>
{{park.state}}
<
/p
>
<
/ion-item
>
<
/ion-list
>
If you recall from the Ionic2Do app, we will define an<ion-list>
component, then define the<ion-item>
that will be auto-generated for us. The repeated items are being supplied by an array named parks, which we will define shortly. Each element of this array is put into a local variable namedpark
. A click handler is also added to the<ion-item>
; it will call a function namedgoParkDetails
and will pass in thepark
variable as its parameter.
If our app is running in iOS mode, disclosure arrows will automatically be added. On Android and Windows, this icon is not added to the list item. If we want to show the right arrow icon that does not display it by default, we can include thedetail-push
attribute. Conversely, if we don’t want to show the right arrow icon, we can use thedetail-none
attribute. We will still need to enable this visual state in the_variables.scss_file by adding:
$item-md-detail-push-show: true;
after we define our$colors
variable.
Returning back to the_park-list.html_file, within the<ion-item>
, we will insert an<ion-thumbnail>
component and set its position in the row by using item-left. The image tag is fairly straight forward. If you used the template for the project, it should have included an_img_directory that also contained a_thumbs_directory. That directory will hold thumbnails for each of our parks. By using Angular’s data binding, we can dynamically set thesrc
for each thumbnail withsrc="assets/img/thumbs/{{park.image}}"
. Next, the park name and state are shown with<h2>
and<p>
tags, respectively, and are also data bound to the park object.
One last thing to do is to remove thepadding
attribute on the<ion-content>
as well. This will enable the list to be the full width of the viewport. With the HTML template updated, we can now focus on the component’s code.
Extending parklist.ts
The first thing that we need to do is to inject our service provider into the component with:
import { ParkData } from '../../providers/park-data';
Initially, the component’s class is completely empty. We will replace it with the following code:
export class ParkListPage {
parks: Array
<
Object
>
= []
constructor(public navCtrl: NavController, public parkData: ParkData) {
parkData.getParks().then(theResult =
>
{
this.parks = theResult;
})
}
goParkDetails(theParkData) {
console.log(theParkData);
}
}
Let’s define theparks
variable that we referenced in our HTML template:
parks: Array
<
Object
>
= [];
Within the parameters of the constructor for the class, we will define a public variableparkData
of typeParkData
.
Next, we will call thegetParks
method on theparkData
. In the past, we might have written something like this to get our park data:
parks = parkData.getParks();
But since we are leveraging the power of Promises, we need to actually write our request for this data as such:
parkData.getParks().then(theResult =
>
{
this.parks = theResult;
}
)
That wraps up the changes to the constructor itself. The last bit of code that was added was a placeholder function for the click event from the<ion-item>
. The method accepts the park data object as a parameter, and simply writes that data to the console. We will focus on this function shortly, but let’s view our work so far by using$ ionic serve
(seeFigure 8-4).
Figure 8-4.National parks app
Once the app has been regenerated, we should see a scrolling list of national parks in a browser, each with a thumbnail, title, and state listed. If you click on an item, the park data will be written out to the JavaScript console. Now that we have this initial screen working, we can turn our attention to creating the details page for that park.
Generating New Pages
With Ionic 2, adding new pages into our app is now a bit more complex. Thankfully, there is a page-generator function available in the CLI. Since we need to generate a park details page, our command will be:
$ ionic g page parkDetails
The CLI will take our camelCase name and convert it into a kebab-case version for the component name. The generator will automatically appendPage
to the class name for us. So if you open the_park-details.ts_file, you will see this class name:
export class ParkDetailsPage { ...
The Ionic Framework has also begun to expose the ability tolazy-load_our components. The primary advantage of doing this is to reduce our applications, overall memory overhead and start times. In order to use this feature, any component that is to be lazy-loaded needs to have a *.module.ts file. If you are wondering why none of the existing components had this file, it is because any component that the application would use during its startup is declared within the _app.module.ts file. Currently, the generation of this file is disabled, but we can expect it to become an option in a future release.
Let’s convert this component to be able to be lazy-loaded. First, we need import theIonicPage
module from theionic-angular
package. Next, we need to add the @IonicPage
decorator just before the@Component
decorator.
Now, we need to create the_park-details.module.ts_file. This file should be in the same directory as the other files for this component. The code for this module is:
import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { ParkDetailsPage } from './park-details';
@NgModule({
declarations: [
ParkDetailsPage,
],
imports: [
IonicPageModule.forChild(ParkDetailsPage),
],
exports: [
ParkDetailsPage
]
})
export class ParkDetailsPageModule {}
In essence, it very similar to what we have in ourapp.module.ts_file, just, it has only the elements that are needed for this single component. Since lazy loading is a new feature, there may be issues with it. If you do not want to have your components lazy-loaded, do not create this file. Also in the companion.ts_file, do not include the@IonicPage()
decorator. We recommend following the progress on this new capability on the Ionic blog (http://blog.ionic.io/\).
IONIC GENERATORS
We also could have used the Ionic CLI generator to create providers by replacing the page flag with the provider flag. In fact, the provider we wrote earlier in the chapter could have been generated in that fashion.
Now, let’s build upon the code in the_park-list.ts_file to enable the navigation to our newly generated page. Another advantage of using lazy-loading is that we no longer have to declare any screen we might navigate to. For a simple application like this one, it isn’t a burden; but in larger applications, this can be an issue. The only change is the page you want to navigate to is now referenced as a simple string, instead of the component itself. The following code includes the goParkDetails
function that will navigate to the park details page and pass along the park information:
goParkDetails(theParkData) {
this.navCtrl.push("ParkDetailsPage", { parkData: theParkData });
}
If you were not using lazy-loading, to navigate to the park details page, you would import the reference to the page:
import { ParkDetailsPage } from '../park-details/park-details';
With that module injected into our component, thegoParkDetails
function will now navigate to the park details page and pass along the park information:
goParkDetails(theParkData) {
this.navCtrl.push(ParkDetailsPage, { parkData: theParkData });
}
Understanding the Ionic Navigation model
Back in Ionic 1, the AngularJS UI-Router was used to navigate between pages. For many apps, this navigation system works fairly well. But if your application has a complex navigation model, using it would become problematic. For example, if we were creating our Ionic Parks app in Ionic 1, we would have to have two distinct URLs for a Parks Details page if we want access it via both a park list screen and a map list screen. These types of issues forced the Ionic team to rebuild their entire navigation model.
The current navigation model is based on a push/pop system. Each new view (or page) is pushed onto a navigation stack, with each view stacking atop the previous one. When the user navigates back through the stack, the current view is removed (popped) from the stack (seeFigure 8-5).
Figure 8-5.Ionic 2 navigation model
This new model makes the navigation model much simpler to work with.
Passing Data Between Pages
In ourgoParkDetails
function, it received theparkData
for the clicked list item. By using theNavParams
module, we can pass this data to the constructor of the new page.
We need to refactor the_park-details.ts_file to support the incoming data. With generated Ionic pages, theNavParams
module fromionic-angular
is already included. Next, in the class definition, we need to add aparkInfo
variable that is typed toObject
.
In this constructor, the navigation parameters are passed in and stored in the variablenavParams
:
import { Component } from '@angular/core';
import { IonicPage, NavController, NavParams } from 'ionic-angular';
@IonicPage()
@Component({
selector: 'page-park-details',
templateUrl: 'park-details.html'
})
export class ParkDetailsPage {
parkInfo: Object;
constructor(public navCtrl: NavController, public navParams: NavParams) {
this.parkInfo = navParams.data.parkData;
console.log(this.parkInfo);
}
}
For now, let’s just write to the console theparkData
that has been piggybacked on this parameter. Our selected park’s data object is saved on thedata
method of thenavParams
. Saving our files and running$ ionic serve
, clicking any item should now change our view and write to the console our data.
You will notice that the Ionic Framework handled the screen-to-screen animation, as well as automatically added a back button in our header to enable the user to navigate back through the navigation stack.
Updating the Park Details Page
Since we can now navigate to the park details page, let’s turn our attention to taking this dynamic data and displaying it.Figure 8-6shows what our initial Park Details screen will look like.
Figure 8-6.The national park details screen
The generated HTML page has some basic tags included, but we are going to replace most it. First, let’s remove the help comment from the top of the page. For the page title, we will replace it with{{parkInfo.name}}
:
<
ion-header
>
<
ion-navbar color="primary"
>
<
ion-title
>
{{parkInfo.name}}
<
/ion-title
>
<
/ion-navbar
>
<
/ion-header
>
<
ion-content
>
<
img src="assets/img/headers/{{parkInfo.image}}"
>
<
h1 padding
>
{{parkInfo.name}}
<
/h1
>
<
ion-card
>
<
ion-card-header
>
Park Details
<
/ion-card-header
>
<
ion-card-content
>
{{parkInfo.data}}
<
/ion-card-content
>
<
/ion-card
>
<
/ion-content
>
One new component we are using on this screen is the<ion-card>
. As the documentation states, “Cards are a great way to display important pieces of content, and are quickly emerging as a core design pattern for apps.” Ionic’s card component is a flexible container that supports headers, footers, and a wide range of other components within the card content itself.
With a basic park details screen in place, go ahead and preview it with$ ionic serve
.
Add a Google Map
As you might expect, an app about the national parks would require them each to be shown on a map of some kind. Unfortunately, there is not an official Google Maps Angular module. There are some third-party efforts, but let’s work instead with the library directly. To do this we will need to include the library in our_index.html_file. Since the terms of use for the Google Maps SDK forbids the storing of the tiles in an offline fashion, we can reference the remotely hosted version:
<
!-- Google Maps --
>
<
script src="http://maps.google.com/maps/api/js"
>
<
/script
>
<
!-- cordova.js required for cordova apps --
>
<
script src="cordova.js"
>
<
/script
>
We can ignore the need for an API key while we are developing our application, but an API key is required for production. You can obtain an API key at theGoogle Developers page. When you get your API key, change the scriptsrc
to include it in the query string.
Adding Additional Typings
Since we are adding in a third-party code library to be used in our app, wouldn’t it be nice to have code hinting support and strong typing for that library? We can do this by extending our TypeScript definitions. The command to do this is:
$ npm install @types/google-maps --save-dev --save-exact
Adding Our Content Security Policy
A Content Security Policy (CSP) is an added layer of security designed to reduce certain types of malicious attacks, including cross-site scripting (XSS). Remember, our hybrid apps are still bound by the same rules that web apps have. As such, we also need to safeguard our applications in a similar manner.
In our_index.html_file, we need to include a CSP:
<
meta http-equiv="Content-Security-Policy" content="default-src * gap://ready; ↵
img-src * 'self' data:; font-src * 'self' data:; script-src 'self'↵
'unsafe-inline' 'unsafe-eval' *; style-src 'self' 'unsafe-inline'↵
*"
>
Since Google Maps transfers its map tiles via the data URI method, our CSP needs to allow for this type of communication. In addition, we will need to add support offont-src
as well as for the Ionicons to work properly. This tag should be placed within the<head>
tag.
Adjust the CSS to support the Google Map
With our library able to be loaded and related data, let’s turn our attention to the map page itself. Inpark-map.html, we need to add a container for the map to be rendered in:
<
ion-content
>
<
div id="map_canvas"
>
<
/div
>
<
/ion-content
>
We need to give it either a CSSid
orclass
in order to apply some CSS styling. Since the tiles are dynamically loaded, ourdiv
has no width or height when it is first rendered. Even as the map tiles are loaded, the width and height of the container are not updated. To solve this, we need to define thisdiv
’s width and height. In the_park-map.scss_file, add the following:
#map_canvas {
width: 100%;
height: 100%;
}
This will give the container an initial value, and our map will be viewable.
Rendering the Google Map
We are going to work on this code in three sections. The first will get the Google Map displaying, the second will be to add markers for each of the National Parks, and the final section will make clicking on the marker navigate to the Park Details page. Switch to the_park-map.ts_file.
We will need to add thePlatform
module to theimport
statementfrom ionic-angular
:
import { Platform, NavController } from 'ionic-angular';
We will use thePlatform
module to make sure everything is ready before setting up the Google map.
Within the class definition, we will define themap
variable as a Google Map. This variable will hold our reference to the Google map:
export class ParkMapPage {
map: google.maps.Map;
Next, we expand the constructor:
constructor( public nav: NavController, public platform: Platform) {
this.map = null;
}
ionViewDidLoad() {
this.platform.ready().then(() =
>
{
this.initializeMap();
});
}
We make sure that we have reference toPlatform
module, then set up a Promise on the platform ready method. Once the platform ready event has fired, we then call ourinitializeMap
function, using the fat arrow syntax:
initializeMap() {
let minZoomLevel = 3;
this.map = new google.maps.Map(document.getElementById('map_canvas'), {
zoom: minZoomLevel,
center: new google.maps.LatLng(39.833, -98.583),
mapTypeControl: false,
streetViewControl: false,
mapTypeId: google.maps.MapTypeId.ROADMAP
});
}
This function will create the new map and assign it to thediv
with the id of"map_canvas"
. We also define some of the various map parameters. These parameters include the zoom level, the center map (in our case, the center of the continental US), the various map controls, and finally the style of the map. The last object method is a custom method where we will store the park information that we will need later in this chapter.
If we run$ ionic serve
, then we should see a map being rendered in the Map tab, as seen inFigure 8-7.
Figure 8-7.Google map within our Ionic app
Add Map Markers
Now that we have a Google map displaying in our mobile app, we can turn to the next task: adding the markers for each national park. The first thing we need to do is inject theParkData
service into our component:
import { ParkData } from '../../providers/park-data';
Next, we will need to add an array that will hold our park data, as well as ensure theparkData
is properly available to the class:
export class ParkMapPage {
parks: Array
<
Park
>
= [];
map: google.maps.Map;
constructor(
public nav: NavController,
public platform: Platform,
public parkData: ParkData
) {
Although we could simply type ourparks
array toany
, let’s properly type to our park’s data structure. To do this, we will need to define the interface. Create a new directory namedinterfaces_within the_app_directory. Within that new directory, create a new file named_park.ts. This file will hold our simple definition for our Park interface. The code for this is:
export interface Park {
id: number;
name: string;
createDate: string;
lat: number;
long: number;
distance: number;
image: string;
state: string;
data: string;
}
This interface will tell the compiler thatPark
data type will have these elements and their associated data types.
Back in the_park-map.ts_file, we will need to import this interface file:
import { Park } from '../../interfaces/park';
That should resolve any warnings in your editor about thePark
data type.
Go ahead and also import this interface in the_park-list.ts_file and change this variable:
parks: Array
<
Object
>
= [];
to:
parks: Array
<
Park
>
= [];
Within theinitializeMap
function, we will need to add the code to actually display our markers.
But rather than use the standard Google marker image, let’s use a marker that looks like the National Parks Service arrowhead logo:
let image = 'assets/img
ps_arrowhead.png';
Then we will get the park data from theparkData
service. Once this Promise is answered, the result will be stored in theparks
array:
this.parkData.getParks().then(theResult =
>
{
this.parks = theResult;
for (let thePark of this.parks) {
let parkPos:google.maps.LatLng = ↵
new google.maps.LatLng (thePark.lat, thePark.long);
let parkMarker:google.maps.Marker = new google.maps.Marker();
parkMarker.setPosition(parkPos);
parkMarker.setMap( this.map);
parkMarker.setIcon(image);
}
})
Our code will loop through this array and generate our markers. Save this file, and ifionic serve
is still running, the app will reload. Select the Map tab, and you now see icons on our map for each of the national parks. Right now, these markers are not interactive. Let’s add that capability.
Making the Markers Clickable
When a user clicks or taps a marker on the map, we want to navigate them to the Park Details page for that markers. To do this we need to inject some of the Navigation modules from Ionic, as well as the actualParkDetailsPage
module. Our new import block will now look like this:
import { Component } from '@angular/core';
import { Platform, NavController, NavParams } from 'ionic-angular';
import { Park } from '../../interfaces/park';
import { ParkData } from '../../providers/park-data';
Within thefor
loop that adds each marker, we will need to add an event listener that will respond to our click, and then navigate to theParkDetailsPage
and pass along the marker’s park data. Unfortunately, the standard Google Map Marker has none of that information. To solve this, we are going to create a custom Map Marker that we can store our park information.
Create a new file,custom-marker.ts, within our_park-map_directory. This new class will extend the basegoogle.maps.Marker
to have one additional value, ourparkData
. We first need to import the Park interface. Then we will export our new class,CustomMapMarker
, which is extended fromgoogle.maps.Marker
. Next, we define ourparkData
variable and assign the type ofPark
. Within the class’s constructor, we will pass in the actual park data. The critical bit of code is thesuper()
. This will tell the class we extendedfrom
to also initialize:
import {Park} from '../../interfaces/park';
export class CustomMapMarker extends google.maps.Marker{
parkData:Park
constructor( theParkData:Park
){
super();
this.parkData = theParkData;
}
}
Save this file, and return back topark-map.ts. If you guessed that we need to import this new class, you would be correct:
import { CustomMapMarker } from './custom-marker';
Now, ourparkMarker
can use ourCustomMapMaker
class in place of thegoogle.maps.Marker
. So this line of code:
let parkMarker:google.maps.Marker = new google.maps.Marker();
becomes this:
let parkMarker:google.maps.Marker = new CustomMapMarker(thePark);
NOTE
We are passing the park’s data into the instance, thus saving our park data within each marker.
Now we can assign our event listener for each marker. But how do we reference the actualparkData
stored within each marker so that we can include it as anavParam
?
We are going to take a shortcut with this block of code. Since we did not define an interface for ourCustomMapMarker
, our compiler does not know about our additional property. But, we can use theany
data type to sidestep this issue. So, if we simply create a local variable,selectedMarker
, with the type ofany
and assign theparkMarker
to it, we will be able to reference theparkData
. Here is the completed fragment:
google.maps.event.addListener(parkMarker, 'click', () =
>
{
let selectedMarker:any = parkMarker;
this.navCtrl.push("ParkDetailsPage", {
parkData: selectedMarker.parkData
});
});
The navigation code should look very familiar from the Park List page. Here is the completeinitializeMap
function:
initializeMap() {
let minZoomLevel:number = 3;
this.map = new google.maps.Map(document.getElementById('map_canvas'), {
zoom: minZoomLevel,
center: new google.maps.LatLng(39.833, -98.583),
mapTypeControl: false,
streetViewControl: false,
mapTypeId: google.maps.MapTypeId.ROADMAP
});
let image = 'assets/img/nps_arrowhead.png';
this.parkData.getParks().then(theResult =
>
{
this.parks = theResult;
for (let thePark of this.parks) {
let parkPos:google.maps.LatLng =↵
new google.maps.LatLng (thePark.lat, thePark.long);
let parkMarker:google.maps.Marker = new CustomMapMarker(thePark);
parkMarker.setPosition(parkPos);
parkMarker.setMap( this.map);
parkMarker.setIcon(image);
google.maps.event.addListener(parkMarker, 'click', () =
>
{
let selectedMarker:any = parkMarker;
this.navCtrl.push("ParkDetailsPage", {
parkData: selectedMarker.parkData
});
});
}
})
}
Save the file, and we should now be able to click a marker and see the Park Details page (Figure 8-8).
Figure 8-8.Custom markers on our Google map
Adding Search
Let’s extend our application a bit further by adding a search bar for the Park List. Ionic has an<ion-searchbar>
as part of the component library. The search bar component will let the user type in the name of a national park and the list of parks will automatically update (Figure 8-9).
Figure 8-9.Search bar component added to our Ionic app
Since we want the search bar to always be available, we need it to be fixed to the top of the screen. We can use the<ion-toolbar>
component to handle this. This component just needs to be after the<ion-navbar>
.
We’ll need to define a model to our<ion-searchbar>
component and bind it to the query string. Also, we need to add a function to handle user input:
<
ion-toolbar
>
<
ion-searchbar [(ngModel)]="searchQuery" (ionInput)="getParks($event)" ↵
(ionClear)="resetList($event)"
>
↵
<
/ion-searchbar
>
<
/ion-toolbar
>
If you are wondering what the[( )]
is doing aroundngModel
, this is a new syntax for Angular’s two-way data binding. The square brackets tell Angular that this is a getter, while the parentheses tell Angular that this is a setter. Putting the two together, you have two-way data binding.
Now in the_park-list.ts_file, let’s define this variable within our class:
export class ParkListPage {
parks: Array
<
Park
>
= []
searchQuery: string = '';
Also in the_park-list.ts_file, we need to add ourgetParks
function:
getParks(event) {
// Reset items back to all of the items
this.parkData.getParks().then(theResult =
>
{
this.parks = theResult;
})
// set queryString to the value of the searchbar
let queryString = event.target.value;
if (queryString !== undefined) {
// if the value is an empty string don't filter the items
if (queryString.trim() == '') {
return;
}
this.parkData.getFilteredParks(queryString).then(theResult =
>
{
this.parks = theResult;
})
}
}
The first part of thegetParks
function ensures that we will be using the original list of parks. If you have coded any filtering functions in the past, you are probably aware that you need to make sure that you are working from an unfiltered list.
Next, we get the query string from the search bar, then check that it is neither undefined nor empty.