Chapter 9.Building a Weather Application

One of other prebuilt templates that the Ionic Framework provides is theside menu template. This design pattern has become increasingly prevalent over the past few years. Rather than having a fixed number of items in a tab bar (which typically remain on-screen using precious screen real estate), this user interface moves many of the navigation options onto a panel that is kept off-screen until the user taps a menu button of some kind. Often this button is either three horizontally stacked lines (aka the hamburger menu) or in some cases three vertical dots. Now, I am not going to get into the pros and cons of this user interface element. I would encourage you to spend a little time researching it on your own to see if it is right for your project. With that said, this is the design pattern we will use to build our Ionic weather application.

Getting Started

Like the two previous projects, we need to generate our initial app. We use another starter template from a GitHub repo. This base template is just the side menu template, with some additional elements in the assets folder:

$ ionic start Ionic2Weather sidemenu

Once this process is complete, again remember to change your working directory:

$ cd Ionic2Weather

And if you are targeting Android, don’t forget to add that platform:

$ ionic platform add android

Let’s take a look at the template in our browser with:

$ ionic serve

Figure 9-1shows what you should see in your browser.

Figure 9-1.The starter side menu template

The template is fairly straightforward. The main screen demonstrates two methods to reveal the side menu: either by the menu icon in the navbar or by the button in the content section. Once you tap either item, the menu animates onto the screen, and the existing content is masked by a semi-transparent overlay. The exact animation will mirror the platform the app is running on. This animation can be overridden of course.

The side menu itself contains two sections: its own navbar, here labeled Menu, and its own content, here showing a list of two items (Page One and Page Two). If you tap outside the side menu, Ionic will automatically dismiss the side menu for you. If you tap on either the Page One or Page Two, the main content of our app will update, and the side menu will be dismissed.

Exploring the Side Menu Template

The side menu template is a bit more complex than the other two templates, so let’s take a long look at the base template before we build our Ionic weather app. There are no changes to the defaultindex.html_file, so there is no need to explore it. Instead, we will start with the_app.html_file. This file can be found in the_src/app_directory. This is our initial HTML that is rendered by our app. Unlike the other two templates, this template’s app component references an external HTML template (_app.html) instead of having the template written inline. As we’ll see, since this template is a bit more complex than the blank or tab templates, it makes sense to have it as an external reference. Here is what the template looks like:

<
ion-menu [content]="content"
>
<
ion-header
>
<
ion-toolbar
>
<
ion-title
>
Menu
<
/ion-title
>
<
/ion-toolbar
>
<
/ion-header
>
<
ion-content
>
<
ion-list
>
<
button menuClose ion-item *ngFor="let p of pages" (click)="openPage(p)"
>

        {{p.title}}

<
/button
>
<
/ion-list
>
<
/ion-content
>
<
/ion-menu
>
<
!-- Disable swipe-to-go-back because it's poor UX to combine STGB
with side menus --
>
<
ion-nav [root]="rootPage" #content swipeBackEnabled="false"
>
<
/ion-nav
>

There are quite a few changes from Ionic 1’s side menu structure. Most notably,<ion-side-menus>and<ion-side-menu-content>tags have been removed and replaced with<ion-menu>and the new page reference system.

Let’s look at the<ion-menu>tag in detail. This tag sets up the content that will be displayed within our side menu. But there is a critical attribute that is also set within this tag, the[content]="content". Here we are setting thecontentproperty of the ion-menu to the variablecontentand not a string. If you look at the last tag in the template you will see a reference to#content, which defines it as a local variable. Now our<ion-menu>can reference it properly and use it for its main content. We will come back to the rest of the attributes of<ion-nav>in a bit.

Next, our template defines the header of the side menu by using the<ion-toolbar>tag. This component is a generic bar that can be placed above or below the content, much like the<ion-navbar>, but without the navigation controls:

<
ion-toolbar
>
<
ion-title
>
Menu
<
/ion-title
>
<
/ion-toolbar
>

Then, we define the side menu content in the<ion-content>tag:

<
ion-content
>
<
ion-list
>
<
button menuClose ion-item *ngFor="let p of pages" (click)="openPage(p)"
>

      {{p.title}}

<
/button
>
<
/ion-list
>
<
/ion-content
>

The template uses an<ion-list>to loop through the pages array and create a list of buttons that are labeled with the object’s title property. It also sets up a click handler,openPageto respond to the user tapping on the button.

Now, let’s return to the<ion-nav>tag and explore it in more detail:

<
ion-nav [root]="rootPage" #content swipeBackEnabled="false"
>
<
/ion-nav
>

The root property is set torootPage. This is defined in the_app.component.ts_file, and will be the property we will update when we need to change our main content. Now let’s turn our attention to the_app.component.ts_file and how these elements are linked together in code.

Exploring the app.component.ts File

Just like the side menu’s HTML template was a bit more complex, so is its_app.components.ts_file. First, we have several more import statements than before:

import { Component, ViewChild } from '@angular/core';
import { Nav, Platform } from 'ionic-angular';
import { StatusBar, Splashscreen } from 'ionic-native';

import { Page1 } from '../pages/page1/page1';
import { Page2 } from '../pages/page2/page2';

In addition to theComponentmodule being imports, we are also importing theViewChildcomponent from@angular/core. Including this import enables our code to use the property decorator@ViewChildto define a reference to a child element. This is the underlying mechanism that allows us to properly reference our nav element.

The next two import statements should look fairly familiar to you. We need theNavandPlatformdirectives imported from Ionic and the reference to the StatusBar from Ionic Native.

The final two imports are the two sample content pages in the template. Since our_app.component.ts_handled the side menu navigation, it needs to have a reference to any screen it may navigate to.

Our@Componentdecorator should also look familiar. As pointed out earlier, it references an external template.

Next, we define ourrootPagevariable to be of data typeany, and set it initially to thePage1component:

rootPage: any = Page1;

For the side menu’s list of pages, that array is defined, as is the structure of its contents:

pages: Array
<
{title: string, component: any}
>

Here is a great example of how TypeScript can provide some code safety by defining the element types.

The constructor then initializes the app (which for the moment handles a status bar call on iOS, once the platform is ready), and sets thepagesarray to our two sample pages.

Finally, we define theopenPagesfunction that is called from the side menu buttons:

openPage(page) {
  // Reset the content nav to have just this page
  // we wouldn't want the back button to show in this scenario
  this.nav.setRoot(page.component);
}

This function takes the page object that is passed into the function and then tells the nav controller to set its root property to thepage.component. By setting the navigation root, we reset the navigation stack history. In doing so, we also prevent Ionic from automatically adding a back button.

Side Menu Options

You might have noticed we actually have not set any options for our side menu.

The menu can be placed either on the left (default) or the right. To change the placement of the side menu, set the side property to the position you want:

<
ion-menu side="right" [content]="content"
>
...
<
/ion-menu
>

The side menu supports three display types:overlay,reveal, andpush. The Ionic Framework will use the correct style based on the currently running platform. The default type for both Material Design and Windows mode isoverlay, andrevealis the default type for iOS mode.

If you want to change it directly on the<ion-menu>tag, use this:

<
ion-menu type="overlay" [content]="content"
>
...
<
/ion-menu
>

However, you can set the side menu type globally via the appconfigobject. This is set in the_app.module.ts_file. The following code snippet sets the base style topush, and then sets themenuTypestyle torevealformd(Android) mode:

imports: [
  IonicModule.forRoot(MyApp, { menuType: 'push',
  platforms: {
   md: {
     menuType: 'reveal',
   }
  }})
]

Displaying the Menu

To actually display our side menu, we can use themenuToggledirective anywhere in our template. Let’s look at thepage1.html_file (_src/pages/page1), and you will find the HTML block that defines the<ion-navbar>:

<
ion-header
>
<
ion-navbar
>
<
button ion-button menuToggle
>
<
ion-icon name="menu"
>
<
/ion-icon
>
<
/button
>
<
ion-title
>
Page One
<
/ion-title
>
<
/ion-navbar
>
<
/ion-header
>

On the<button>element, you will see themenuToggledirective has been applied. This is all we need to do in order for Ionic to display our side menu. This does not define the visible element for our side menu button. That is done in this example by the<ion-icon>and setting the name tomenu.

In fact, if you look further in the_page1.html_code, you will see this:

<
button ion-button secondary menuToggle
>
Toggle Menu
<
/button
>

Here, the button element has themenuToggledirective applied to it, thus allowing it to open the side menu as well. Although the Ionic Framework handles a modest amount of work for us in opening and closing the side menu, there may be times when we need to directly close the side menu. We add this functionality by using themenuClosedirective on our element:

<
button ion-button menuClose="left"
>
Close Side Menu
<
/button
>

That covers most of the basics of the side menu template. Let’s get started building our actual application.

Converting the Template

Before we begin converting the template to our needs, we need to replace our src directory with some starter files. 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/Ionic2Weather\, 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 run ionic serve again, there should no visual change to the app.

Now, let’s generate our two pages that our app will use, as well as the data providers:

$ ionic g page weather

$ ionic g page locations

Next, let’s add in our two providers. The first provider will eventually be the provider that will pull live weather data from darksky.net (formerly Forecast.io):

$ ionic g provider WeatherService

Once the command has finished generating the template provider, we need to create our second provider. This provider will be used to take a human-friendly location, like San Diego, and turn it into a corresponding latitude and longitude required by darksky.net api. We will touch on both of these providers later in this chapter:

$ ionic g provider GeocodeService

Now, with our new base pages and providers in place, let’s convert this template to use them. Open_app.module.ts_in your editor.

In the import section, we can remove the reference to Page1 and Page2 and add in the imports to our two newly created pages:

import { WeatherPage } from '../pages/weather/weather';
import { LocationsPage } from '../pages/locations/locations';

Also, update the declarations andentryComponentsarrays to reflect these new pages.

In the past, we would have had to manually add the two providers we just generated. With improvements to the Ionic CLI, they are now automatically added to the_app.module.ts_file for us.

With those changes made, we can turn to_app.component.ts_and begin to update it. First, replace the two Page imports with our new pages:

import { WeatherPage } from '../pages/weather/weather';
import { LocationsPage } from '../pages/locations/locations';

Continuing further down in the file, we will see the line of code that defines therootPagevariable for our application. We need to change this to reference ourWeatherPageinstead of Page1:

rootPage: any = WeatherPage;

Our next bit of code to modify is thepagesarray. We are going to be extending this array quite a bit over the course of building our app, but for now, we will do just the basics.

Since this app is written in TypeScript, we need to be careful when modifying templates that we properly update any variable definitions. For example, thepagesvariable is defined to be an array that contains objects with a title property that is of typestring, and component property that is of typeany. Since we would like to display an icon next to each item in my side menu list, we will need to update the definition to:

pages: Array
<
{title: string, component: any, icon: string}
>

Let’s update thepagesarray to reference the proper component, as well as add an icon value and change the title. Here is the new array:

this.pages = [
 { title: 'Edit Locations', component: LocationsPage, icon: 'create' },
 { title: 'Current Location', component: WeatherPage, icon: 'pin' }
];

Later, we will extend this array to hold our saved locations and their latitudes and longitudes.

If you ran the app in its current form, you would only see the weather page and have no ability to access the side menu. Let’s address that issue now and make a few other changes to the navbar.

Open_weather.html_in your editor.

First, we need to add in the side menu button within the<ion-navbar>:

<
ion-navbar
>
<
button ion-button menuToggle
>
<
ion-icon name="menu"
>
<
/ion-icon
>
<
/button
>
<
ion-title
>
weather
<
/ion-title
>
<
/ion-navbar
>

Next, change the<ion-title>to displayCurrent Location. Once we have geolocation enabled, we will return to this tag later and have it display the actual location name:

<
ion-title
>
Current Location
<
/ion-title
>

If you save the file now, you should see the menu icon on the left and have the ability to display our side menu. Let’s update the header in the_locations.html_as well:

<
ion-header
>
<
ion-navbar
>
<
button ion-button menuToggle
>
<
ion-icon name="menu"
>
<
/ion-icon
>
<
/button
>
<
ion-title
>
Edit Locations
<
/ion-title
>
<
/ion-navbar
>
<
/ion-header
>

Now, with that code added, this page will be able to display the side menu as well. Switch back to the_app.html_file and update the code to show our icons next to each list item. We just need to add a<ion-icon>tag and set its name to the icon property of the list element. We will also need to add some padding to the right of the icon so that the text is not directly next to the icon itself. The quickest way is to just add a space between the end tag</ion-icon>and the opening mustache tags of{{p.title}}:

<
ion-list
>
<
button menuClose ion-item *ngFor="let p of pages" (click)="openPage(p)"
>
<
ion-icon name="{{p.icon}}"
>
<
/ion-icon
>
 {{p.title}}

<
/button
>
<
/ion-list
>

If we wanted better control over the spacing between the icon and the location name, we could have set it in our CSS.

Another minor tweak is to set the<ion-title>to:

<
ion-title
>
Ionic Weather
<
/ion-title
>

instead of the defaultMenu.

Our final tweak is to add a reference to where we are getting the weather data from (because you actually read the terms and conditions, right?). After the<ion-list>, add this bit of markup:

<
p
>
<
a href="https://darksky.net/poweredby/"
>
Powered by Dark Sky
<
/a
>
<
/p
>

Save this file, and our side menu should now show our icons and have the ability to navigate to our two pages.

Mocking Up Our Weather Provider

Our next step is to get some weather data that we can use in our application. We are going to approach this in two phases. We are going to first load in static weather data. This will allow us to do some initial screen layout. Then we will hook our application up into a live data source.

There is a static data file that you can use that is included in the_assets_directory. This file is a snapshot of some darksky.net weather data.

Now, let’s update the weather provider code to have a method to get this data. Open the_weather-service.ts_file and replace it with this code:

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

@Injectable()
export class WeatherServiceProvider {
  data: any = null;

  constructor(public http: Http) {
    console.log('Hello WeatherService Provider');
  }

  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);
        });
    });
  }
}

After theloadmethod, add in this function:

getWeather() {
  return this.load().then(data =
>
 {
    return data;
  });
}

Although the method does call theloadfunction, I like having it as a separate method. This gives us some flexibility later if our application requires it.

Laying Out the Weather Data

Before we can display our mock weather data, we need to have access to it within the context of that page. Open the_weather.ts_file. The first thing we will need to do is import theWeatherServiceprovider:

import { WeatherServiceProvider } from '../../providers/weather-service/weather-service';

Next, we need to add three variables to hold our weather data with the class definition. We will type these asany, so it will not generate any TypeScript errors:

theWeather: any = {};
currentData: any = {};
day1: any = {};
day2: any = {};
day3: any = {};

The reason there arecurrentDataanddailyvariables is that the actual JSON data structure from darksky.net is a bit nested. So rather than having to traverse this in our template, we can define thecurrentDataanddailyvariables as pointers.

Next, pass a reference to our WeatherService into our constructor:

constructor(public navCtrl: NavController, public navParams: NavParams,
public weatherService: WeatherServiceProvider) { }

Now, within our constructor we can call thegetWeathermethod on theweatherDataprovider:

this.weatherService.getWeather().then(theResult =
>
 {
  this.theWeather = theResult;
  this.currentData = this.theWeather.currently;
  this.day1 = this.theWeather.daily.data[0];
  this.day2 = this.theWeather.daily.data[1];
  this.day3 = this.theWeather.daily.data[2];
});

With the data now loaded, let’s turn our attention to the HTML template. Open_weather.html_again so we can begin updating the template to show our weather data.

For the layout of our data, we are going to use the Ionic Grid component. From the Ionic documentation:

Ionic’s grid system is based on flexbox, a CSS feature supported by all devices that Ionic supports. The grid is composed of three units — grid, rows and columns. Columns will expand to fill their row, and will resize to fit additional columns.

To begin, we will add a<ion-grid>tag. Next, we will define our first row that will contain the current temperature and conditions:

<
ion-grid
>
<
ion-row
>
<
ion-col col-12
>
<
h1
>
{{currentData.temperature | number:'.0-0'}}
&
deg;
<
/h1
>
<
p
>
{{currentData.summary}}
<
/p
>
<
/ion-col
>
<
ion-row
>
<
/ion-grid
>

If you take a look at the actual_data.json_file, you will see the temperature values are actually saved out to two decimal places. I doubt we need this level of accuracy for our app. To solve this, we can use one the of the built-inPipefunctions in Angular to round our values for us.

WHAT IS A PIPE?

A pipe takes in data as input and transforms it into the desired output. Angular includes several stock pipes such as DatePipe, UpperCasePipe, LowerCasePipe, CurrencyPipe, and PercentPipe.

By adding| number:'.0-0'after the data binding forcurrentData.temperature, our data will go from 58.59 to 59.

Next, we will add another row that will display the high and low temperatures for the next three days:

<
ion-row
>
<
ion-col col-4
>

    {{day1.temperatureMax | number:'.0-0'}}
&
deg;
<
br
>

    {{day1.temperatureMin | number:'.0-0'}}
&
deg;

<
/ion-col
>
<
ion-col col-4
>

    {{day2.temperatureMax | number:'.0-0'}}
&
deg;
<
br
>

    {{day2.temperatureMin | number:'.0-0'}}
&
deg;

<
/ion-col
>
<
ion-col col-4
>

    {{day3.temperatureMax | number:'.0-0'}}
&
deg;
<
br
>

    {{day3.temperatureMin | number:'.0-0'}}
&
deg;

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

Now if you save the file and try to run the app, you will encounter an error. This error occurs because our data is being loaded via a Promise and is not initially available to the template. To fix this issue, we can add anngIfdirective to tell the template not to display our grid until we have our data. However, we could have set our variables to some initial values, and thus allowed the template to render. Any of our non–data-bound elements would be shown, like the ° symbol. By wrapping the entire grid in anngIf, we can control the display of everything. So our<ion-grid>tag becomes:

<
ion-grid *ngIf="currentData.temperature != undefined"
>

Saving the file again, our template will correctly render once the data has been properly loaded.

Figure 9-2.Our mock weather data being displayed

Loading Feedback: Loading Dialogs and Pull to Refresh

It would be nice if there was some feedback that the app was loading the weather data. Although the load time might be brief when using our mock data, once we hook it up to a live data source, it might take a moment or two.

Ionic has aLoadingControllercomponent that we can easily add into our app. Update the import statement for ionic-angular to include Loading.

import { NavController, NavParams, LoadingController } from 'ionic-angular';

Next, in the class definition add in a loading variable of typeLoading:

export class WeatherPage {
  theWeather: any = {};
  currentData: any = {};
  this.day1 = this.theWeather.daily.data[0];
  this.day2 = this.theWeather.daily.data[1];
  this.day3 = this.theWeather.daily.data[2];
  loader: LoadingController;

We will also need to pass this module into our constructor:

constructor(public navCtrl: NavController, 
            public navParams: NavParams,
            public weatherService: WeatherServiceProvider, 
            public loadingCtrl: LoadingController) {...

Now we can create our loading dialog component instance. Add this code before making theweatherService.getWeather()call:

let loader = this.loadingCtrl.create({
  content: "Loading weather data...",
  duration: 3000
});

We are setting a duration for the dialog for the moment for testing purposes since our data is still being loaded locally. By setting it to three seconds, it gives us a chance to see it before it is dismissed. To display the loading dialog, we need to simply tell the loader instance to show the dialog via thepresentmethod:

loader.present();

For more information about theLoadingcomponent, see theIonic Framework website.

By saving this file and running the app, the loading dialog will appear for three seconds and then dismiss itself (Figure 9-3).

Figure 9-3.The loading dialog

Since we are working with dialogs and data updates, let’s add in the code to allow us to use theRefreshercomponent (aka Pull To Refresh). This is a popular UX method to allow users to force a data refresh without having to have an on-screen control to enable it.

In the_weather.html_file, after the<ion-content>, we can add:

<
ion-refresher (ionRefresh)="doRefresh($event)"
>
<
ion-refresher-content
    pullingIcon="arrow-dropdown"
    pullingText="Pull to refresh"
    refreshingSpinner="circles"
    refreshingText="Refreshing..."
>
<
/ion-refresher-content
>
<
/ion-refresher
>

This will define the<ion-refresher>and<ion-refresher-content>. Beyond setting the text that is displayed, you can also define which spinner animation you want. The built-in options areios,ios-small,bubbles,circles,crescent, anddots. When the pull to refresh component is triggered it will call thedoRefreshmethod. Let’s add in thedoRefreshmethod to the_weather.ts_file.

First, we need to include this component as part of the imports from Ionic:

import { NavController, NavParams, LoadingController, Refresher } from 
'ionic-angular';

Then include a variable to reference the Refresher in the class definition:

export class WeatherPage {
  theWeather: any = {};
  currentData: any = {};
  daily: any = {};
  loader: LoadingController;
  refresher: Refresher;

Now we will add in thedoRefreshmethod to our class:

doRefresh(refresher) {
  setTimeout(() =
>
 {
    refresher.complete();
  }, 2000);
}

For the moment, we are just triggering a simple timeout call for two seconds. We will return to this method once we have live data enabled, but this will allow us to see the Refresher’s UI in action.

Adding GeoLocation

The Dark Sky API requires us to pass in a latitude and longitude value in order to get the weather forecast. We can obtain this set of data through the use of Cordova’s Geolocation plug-in. To add this plug-in to our app, we need to use this command in the terminal:

$ ionic cordova plugin add cordova-plugin-geolocation

The CLI will first prompt you to install the @ionic/cli-plugin-cordova plug-in, respond Yes, then it will add the plug-in for all the install platforms. Note that this command only installs the plug-in itself; it does not write any of the actual code to use this plug-in. Next, we also need to install the Ionic Native module for Geolocation.

$ npm install --save @ionic-native/geolocation

This will include the module in our_node_modules_directory, but we will still need to manually include into our app. In the_app.module.ts_file, we need to import and add the Geolocation plug-in provider to our@NgModuledeclaration.

import { Geolocation } from '@ionic-native/geolocation';


...

providers: [
  StatusBar,
  SplashScreen,

Geolocation
,
  { provide: ErrorHandler, useClass: IonicErrorHandler },
  WeatherServiceProvider,
  GeocodeServiceProvider]

Openweather.ts, and first, let’s import the Geolocation module from Ionic Native:

import { Geolocation } from '@ionic-native/geolocation';

as well as adding to our parameters in the constructor.

If you recall, Ionic Native acts as an Angular wrapper for your Cordova plug-ins.

Next, in the constructor, we will call thegetCurrentPositionfunction on the Geolocation module. For the moment, it will just write out your current latitude and longitude to the console:

geolocation.getCurrentPosition().then(pos =
>
 {
  console.log('lat: ' + pos.coords.latitude + ', lon: ' + pos.coords.longitude);
});

Since making GPS requests are battery consuming, we should save this result and use it instead. Instead of saving this to a generic object, let’s define a variable that implements the CurrentLoc interface.

To do this, we need to define that interface. Create a new directory namedinterfaces_within the_src_directory. Within that new directory, create a new file named_current-loc.ts. This file will hold our simple definition for our CurrentLoc interface. The code for this is:

export interface CurrentLoc {
  lat: number;
  lon: number;
  timestamp?: number;
}

This interface will tell the compiler that it needs both alatandlongproperty with their allowed values as numbers. It also tells the compiler that an optional propertytimestampcan be passed. This is done by adding in the?after the property name.

Returning toweather.ts, we need to import this interface for use in our class:

import { CurrentLoc } from '../../interfaces/current-loc';

Now, we can create acurrentLocvariable that implements the CurrentLoc interface:

currentLoc: CurrentLoc = {lat:0 , lon: 0};

OurgetCurrentPositioncall can now save our result into ourcurrentLocvariable:

geolocation.getCurrentPosition().then(pos =
>
 {
  console.log('lat: ' + pos.coords.latitude + ', lon: ' + pos.coords.longitude);
  this.currentLoc.lat = pos.coords.latitude;
  this.currentLoc.lon = pos.coords.longitude;
  this.currentLoc.timestamp = pos.timestamp;
});

We will save the timestamp in case we want to check the age of the data and trigger a refresh. I will leave that to you as a programming challenge once the app is complete. Now that we know where on this planet you are, let’s find out your weather.

results matching ""

    No results matching ""